diff --git a/base/v0_7_exp/schema.go b/base/v0_7/schema.go similarity index 99% rename from base/v0_7_exp/schema.go rename to base/v0_7/schema.go index 4f4e05ed3..0da3b892d 100644 --- a/base/v0_7_exp/schema.go +++ b/base/v0_7/schema.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 type Cex struct { Enabled *bool `yaml:"enabled"` diff --git a/base/v0_7_exp/translate.go b/base/v0_7/translate.go similarity index 99% rename from base/v0_7_exp/translate.go rename to base/v0_7/translate.go index 2b25dfca4..0b4142826 100644 --- a/base/v0_7_exp/translate.go +++ b/base/v0_7/translate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 import ( "fmt" diff --git a/base/v0_7_exp/translate_test.go b/base/v0_7/translate_test.go similarity index 99% rename from base/v0_7_exp/translate_test.go rename to base/v0_7/translate_test.go index d3b620a39..3be86234e 100644 --- a/base/v0_7_exp/translate_test.go +++ b/base/v0_7/translate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 import ( "fmt" diff --git a/base/v0_7_exp/util.go b/base/v0_7/util.go similarity index 99% rename from base/v0_7_exp/util.go rename to base/v0_7/util.go index b4813d13d..9899a44d9 100644 --- a/base/v0_7_exp/util.go +++ b/base/v0_7/util.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 import ( common "github.com/coreos/butane/config/common" diff --git a/base/v0_7_exp/validate.go b/base/v0_7/validate.go similarity index 99% rename from base/v0_7_exp/validate.go rename to base/v0_7/validate.go index ca89e08bd..4b9bc3853 100644 --- a/base/v0_7_exp/validate.go +++ b/base/v0_7/validate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 import ( "strings" diff --git a/base/v0_7_exp/validate_test.go b/base/v0_7/validate_test.go similarity index 99% rename from base/v0_7_exp/validate_test.go rename to base/v0_7/validate_test.go index aacb3b297..f65eaeadd 100644 --- a/base/v0_7_exp/validate_test.go +++ b/base/v0_7/validate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v0_7_exp +package v0_7 import ( "fmt" diff --git a/base/v0_8_exp/schema.go b/base/v0_8_exp/schema.go new file mode 100644 index 000000000..c5c1046a8 --- /dev/null +++ b/base/v0_8_exp/schema.go @@ -0,0 +1,271 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +type Cex struct { + Enabled *bool `yaml:"enabled"` +} + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Cex Cex `yaml:"cex"` + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + Discard *bool `yaml:"discard"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + OpenOptions []string `yaml:"open_options"` + Options []string `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []string `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` + Advertisement *string `yaml:"advertisement"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + 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 { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/base/v0_8_exp/translate.go b/base/v0_8_exp/translate.go new file mode 100644 index 000000000..0c3ebbdab --- /dev/null +++ b/base/v0_8_exp/translate.go @@ -0,0 +1,564 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// ToIgn3_7Unvalidated translates the config to an Ignition config. It also returns the set of translations +// it did so paths in the resultant config can be tracked back to their source in the source config. +// No config validation is performed on input or output. +func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + 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) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + 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) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + 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) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir) + if err != nil { + r.AddOnError(c.Append(keyFileIndex), err) + continue + } + for _, line := range regexp.MustCompile("\r?\n").Split(string(sshKeys), -1) { + if line == "" { + continue + } + tm.AddTranslation(c.Append(keyFileIndex), path.New("json", "sshAuthorizedKeys", len(to.SSHAuthorizedKeys))) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateDropin) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + 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 +} + +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(options.srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(options.srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(options.destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + // 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 { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + 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")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + 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 + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/base/v0_8_exp/translate_test.go b/base/v0_8_exp/translate_test.go new file mode 100644 index 000000000..f9f8cc923 --- /dev/null +++ b/base/v0_8_exp/translate_test.go @@ -0,0 +1,2519 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzz_gz := "data:;base64,H4sIAAAAAAAC/6oajAAQAAD//5tA8d+VAAAA" + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + random_b64 := "data:;base64,wJxsAYlppb9X5Bv0Sl+3OVCjI6c=" + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzz_gz), + Compression: util.StrToPtr("gzip"), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzz_gz), + Compression: util.StrToPtr("gzip"), + }, + { + Source: util.StrToPtr(random_b64), + Compression: util.StrToPtr(""), + }, + { + Source: util.StrToPtr(random_b64), + Compression: util.StrToPtr(""), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outDirs []types.Directory + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:;base64,H4sIAAAAAAAC/yopSk3VLy5NSsksIptKy8xJBQQAAP//gkRzjkgAAAA="), + Compression: util.StrToPtr("gzip"), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + 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/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + 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 { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer listener.Close() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_7Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, test.outDirs, actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLuks test translating the butane storage.luks.clevis.tang.[i] arguments to ignition storage.luks.clevis.tang.[i] entries. +func TestTranslateTang(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // Luks with tang and all options set, returns a valid ignition config with the same options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + report string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid inline keys", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + }, + "", + sshKeyDir, + }, + { + "valid multiple local key files", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName, sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid local and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid empty local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "", + sshKeyDir, + }, + { + "valid blank local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "missing local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(sshKeyDir, sshKeyNonExistingFileName) + ": " + osNotFound + "\n", + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(randomDir, sshKeyFileName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + report string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "non existing dropin contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty dropin contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_7 tests the config.ToIgn3_7 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_7(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/base/v0_8_exp/util.go b/base/v0_8_exp/util.go new file mode 100644 index 000000000..35b9b24e4 --- /dev/null +++ b/base/v0_8_exp/util.go @@ -0,0 +1,158 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +import ( + common "github.com/coreos/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/base/v0_8_exp/validate.go b/base/v0_8_exp/validate.go new file mode 100644 index 000000000..4b37c8f18 --- /dev/null +++ b/base/v0_8_exp/validate.go @@ -0,0 +1,106 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +import ( + "strings" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} diff --git a/base/v0_8_exp/validate_test.go b/base/v0_8_exp/validate_test.go new file mode 100644 index 000000000..e05a7de4e --- /dev/null +++ b/base/v0_8_exp/validate_test.go @@ -0,0 +1,412 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v0_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestValidateUnit tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/config/config.go b/config/config.go index 2bab7bc8e..de06b7578 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,8 @@ import ( fcos1_4 "github.com/coreos/butane/config/fcos/v1_4" fcos1_5 "github.com/coreos/butane/config/fcos/v1_5" fcos1_6 "github.com/coreos/butane/config/fcos/v1_6" - fcos1_7_exp "github.com/coreos/butane/config/fcos/v1_7_exp" + fcos1_7 "github.com/coreos/butane/config/fcos/v1_7" + fcos1_8_exp "github.com/coreos/butane/config/fcos/v1_8_exp" fiot1_0 "github.com/coreos/butane/config/fiot/v1_0" fiot1_1_exp "github.com/coreos/butane/config/fiot/v1_1_exp" flatcar1_0 "github.com/coreos/butane/config/flatcar/v1_0" @@ -73,10 +74,11 @@ func init() { RegisterTranslator("fcos", "1.4.0", fcos1_4.ToIgn3_3Bytes) RegisterTranslator("fcos", "1.5.0", fcos1_5.ToIgn3_4Bytes) RegisterTranslator("fcos", "1.6.0", fcos1_6.ToIgn3_5Bytes) - RegisterTranslator("fcos", "1.7.0-experimental", fcos1_7_exp.ToIgn3_6Bytes) + RegisterTranslator("fcos", "1.7.0", fcos1_7.ToIgn3_6Bytes) + RegisterTranslator("fcos", "1.8.0-experimental", fcos1_8_exp.ToIgn3_7Bytes) RegisterTranslator("flatcar", "1.0.0", flatcar1_0.ToIgn3_3Bytes) RegisterTranslator("flatcar", "1.1.0", flatcar1_1.ToIgn3_4Bytes) - RegisterTranslator("flatcar", "1.2.0-experimental", flatcar1_2_exp.ToIgn3_6Bytes) + RegisterTranslator("flatcar", "1.2.0-experimental", flatcar1_2_exp.ToIgn3_7Bytes) RegisterTranslator("openshift", "4.8.0", openshift4_8.ToConfigBytes) RegisterTranslator("openshift", "4.9.0", openshift4_9.ToConfigBytes) RegisterTranslator("openshift", "4.10.0", openshift4_10.ToConfigBytes) @@ -94,9 +96,9 @@ func init() { RegisterTranslator("openshift", "4.22.0-experimental", openshift4_22_exp.ToConfigBytes) RegisterTranslator("r4e", "1.0.0", r4e1_0.ToIgn3_3Bytes) RegisterTranslator("r4e", "1.1.0", r4e1_1.ToIgn3_4Bytes) - RegisterTranslator("r4e", "1.2.0-experimental", r4e1_2_exp.ToIgn3_6Bytes) + RegisterTranslator("r4e", "1.2.0-experimental", r4e1_2_exp.ToIgn3_7Bytes) RegisterTranslator("fiot", "1.0.0", fiot1_0.ToIgn3_4Bytes) - RegisterTranslator("fiot", "1.1.0-experimental", fiot1_1_exp.ToIgn3_6Bytes) + RegisterTranslator("fiot", "1.1.0-experimental", fiot1_1_exp.ToIgn3_7Bytes) RegisterTranslator("rhcos", "0.1.0", unsupportedRhcosVariant) } diff --git a/config/fcos/v1_7_exp/schema.go b/config/fcos/v1_7/schema.go similarity index 95% rename from config/fcos/v1_7_exp/schema.go rename to config/fcos/v1_7/schema.go index b58c15b1f..1266e6ee0 100644 --- a/config/fcos/v1_7_exp/schema.go +++ b/config/fcos/v1_7/schema.go @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v1_7_exp +package v1_7 import ( - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_7" ) type Config struct { diff --git a/config/fcos/v1_7_exp/translate.go b/config/fcos/v1_7/translate.go similarity index 99% rename from config/fcos/v1_7_exp/translate.go rename to config/fcos/v1_7/translate.go index 80d84fe57..1e917f0ea 100644 --- a/config/fcos/v1_7_exp/translate.go +++ b/config/fcos/v1_7/translate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v1_7_exp +package v1_7 import ( "fmt" diff --git a/config/fcos/v1_7_exp/translate_test.go b/config/fcos/v1_7/translate_test.go similarity index 99% rename from config/fcos/v1_7_exp/translate_test.go rename to config/fcos/v1_7/translate_test.go index 1f9164d5a..8f42671e6 100644 --- a/config/fcos/v1_7_exp/translate_test.go +++ b/config/fcos/v1_7/translate_test.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v1_7_exp +package v1_7 import ( "fmt" "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_7" "github.com/coreos/butane/config/common" confutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" diff --git a/config/fcos/v1_7_exp/validate.go b/config/fcos/v1_7/validate.go similarity index 98% rename from config/fcos/v1_7_exp/validate.go rename to config/fcos/v1_7/validate.go index b98935b87..2965bc7a6 100644 --- a/config/fcos/v1_7_exp/validate.go +++ b/config/fcos/v1_7/validate.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v1_7_exp +package v1_7 import ( "regexp" "strings" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_7" "github.com/coreos/butane/config/common" "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/util" diff --git a/config/fcos/v1_7_exp/validate_test.go b/config/fcos/v1_7/validate_test.go similarity index 99% rename from config/fcos/v1_7_exp/validate_test.go rename to config/fcos/v1_7/validate_test.go index 1a4cf6881..cc31f95dd 100644 --- a/config/fcos/v1_7_exp/validate_test.go +++ b/config/fcos/v1_7/validate_test.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v1_7_exp +package v1_7 import ( "fmt" "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_7" "github.com/coreos/butane/config/common" "github.com/coreos/ignition/v2/config/shared/errors" diff --git a/config/fcos/v1_8_exp/schema.go b/config/fcos/v1_8_exp/schema.go new file mode 100644 index 000000000..b27c59946 --- /dev/null +++ b/config/fcos/v1_8_exp/schema.go @@ -0,0 +1,53 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v1_8_exp + +import ( + base "github.com/coreos/butane/base/v0_8_exp" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Cex base.Cex `yaml:"cex"` + Discard *bool `yaml:"discard"` + Device *string `yaml:"device"` + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} + +type Grub struct { + Users []GrubUser `yaml:"users"` +} + +type GrubUser struct { + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` +} diff --git a/config/fcos/v1_8_exp/translate.go b/config/fcos/v1_8_exp/translate.go new file mode 100644 index 000000000..62db98821 --- /dev/null +++ b/config/fcos/v1_8_exp/translate.go @@ -0,0 +1,413 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v1_8_exp + +import ( + "fmt" + "strings" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_7Unvalidated translates the config to an Ignition config. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_7Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + for j, partition := range disk.Partitions { + // check for reserved partlabels + if partition.Label != nil { + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", j, "label"), common.ErrWrongPartitionNumber) + } + } + } + } + } + + retp, tsp, rp := c.handleUserGrubCfg(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + return ret, ts, r +} + +// ToIgn3_7 translates the config to an Ignition config. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_7Bytes translates from a v1.6 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or +// warnings in the source and resultant config. If the report has fatal errors or it encounters other problems +// translating, an error is returned. +func ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 || util.IsTrue(c.BootDevice.Luks.Cex.Enabled) + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + case *layout == "s390x-eckd" || *layout == "s390x-virt" || *layout == "s390x-zfcp": + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + var luksDevice string + switch { + //Luks Device for dasd and zFCP-scsi + case layout != nil && *layout == "s390x-eckd": + luksDevice = *c.BootDevice.Luks.Device + "2" + case layout != nil && *layout == "s390x-zfcp": + luksDevice = *c.BootDevice.Luks.Device + "4" + case wantMirror: + luksDevice = "/dev/md/md-root" + default: + luksDevice = "/dev/disk/by-partlabel/root" + } + if util.IsTrue(c.BootDevice.Luks.Cex.Enabled) { + cex, ts2, r2 := translateBootDeviceLuksCex(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Cex: cex, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("cex"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } else { + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Clevis + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func translateBootDeviceLuksCex(from BootDeviceLuks, options common.TranslateOptions) (to types.Cex, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Cex + tm, r = translate.Prefixed(tr, "enabled", &from.Cex.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "enabled", &from.Cex.Enabled, &to.Enabled) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func (c Config) handleUserGrubCfg(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + yamlPath := path.New("yaml", "grub", "users") + if len(c.Grub.Users) == 0 { + // No users + return rendered, ts, r + } + + // create boot filesystem + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, + types.Filesystem{ + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }) + + userCfgContent := []byte(buildGrubConfig(c.Grub)) + src, compression, err := baseutil.MakeDataURL(userCfgContent, nil, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return rendered, ts, r + } + + // Create user.cfg file and add it to rendered config + rendered.Storage.Files = append(rendered.Storage.Files, + types.File{ + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(src), + Compression: compression, + }, + }, + }, + }) + + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + return rendered, ts, r +} + +func buildGrubConfig(gb Grub) string { + // Process super users and corresponding passwords + allUsers := []string{} + cmds := []string{} + + for _, user := range gb.Users { + // We have already validated that user.Name and user.PasswordHash are non-empty + allUsers = append(allUsers, user.Name) + // Command for setting users password + cmds = append(cmds, fmt.Sprintf("password_pbkdf2 %s %s", user.Name, *user.PasswordHash)) + } + superUserCmd := fmt.Sprintf("set superusers=\"%s\"\n", strings.Join(allUsers, " ")) + return "# Generated by Butane\n\n" + superUserCmd + strings.Join(cmds, "\n") + "\n" +} diff --git a/config/fcos/v1_8_exp/translate_test.go b/config/fcos/v1_8_exp/translate_test.go new file mode 100644 index 000000000..1f97dc7ee --- /dev/null +++ b/config/fcos/v1_8_exp/translate_test.go @@ -0,0 +1,1639 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v1_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // LUKS, x86_64, with Tang set for offline provisioning + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "advertisement"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "advertisement")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateGrub tests translating the Butane config Grub section. +func TestTranslateGrub(t *testing.T) { + // Some tests below have the same translations + translations := []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "compression")}, + } + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // config with 1 user + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr("data:,%23%20Generated%20by%20Butane%0A%0Aset%20superusers%3D%22root%22%0Apassword_pbkdf2%20root%20grub.pbkdf2.sha512.10000.874A958E526409...%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + // config with 2 users (and 2 different hashes) + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root1", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + { + Name: "root2", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874B829D126209..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr("data:;base64,H4sIAAAAAAAC/3zMsQrCMBDG8b1PcdT9SI62JoODRfExJCGngtCEuwTx7UWyiss3fH/47eDCG0uonCC+YW01bDwMyhW0FZamLHoYJedq4bs0DiWovrKka4nPdCPo8S4tYn9QH2G2hNYYY9Dtp6Of3XmmZTIeEX8C9BdYHfmTpYU68AkAAP//Mp8bt7YAAAA="), + Compression: util.StrToPtr("gzip"), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/config/fcos/v1_8_exp/validate.go b/config/fcos/v1_8_exp/validate.go new file mode 100644 index 000000000..0c9ecb2d7 --- /dev/null +++ b/config/fcos/v1_8_exp/validate.go @@ -0,0 +1,140 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v1_8_exp + +import ( + "regexp" + "strings" + + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const rootDevice = "/dev/disk/by-id/coreos-boot-disk" + +var allowedMountpoints = regexp.MustCompile(`^/(etc|var)(/|$)`) +var dasdRe = regexp.MustCompile("(/dev/dasd[a-z]$)") +var sdRe = regexp.MustCompile("(/dev/sd[a-z]$)") + +// We can't define a Validate function directly on Disk because that's defined in base, +// so we use a Validate function on the top-level Config instead. +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + for i, disk := range conf.Storage.Disks { + if disk.Device != rootDevice && !util.IsTrue(disk.WipeTable) { + for p, partition := range disk.Partitions { + if partition.Number == 0 && partition.Label != nil { + r.AddOnWarn(c.Append("storage", "disks", i, "partitions", p, "number"), common.ErrReuseByLabel) + } + } + } + } + for i, fs := range conf.Storage.Filesystems { + if fs.Path != nil && !allowedMountpoints.MatchString(*fs.Path) && util.IsTrue(fs.WithMountUnit) { + r.AddOnError(c.Append("storage", "filesystems", i, "path"), common.ErrMountPointForbidden) + } + } + return +} + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnError(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + // Nothing to do + case "s390x-eckd": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !dasdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-zfcp": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !sdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-virt": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayout) + } + + // Mirroring the boot disk is not supported on s390x + if strings.HasPrefix(*d.Layout, "s390x") && len(d.Mirror.Devices) > 0 { + r.AddOnError(c.Append("layout"), common.ErrMirrorNotSupport) + } + } + + // CEX is only valid on s390x and incompatible with Clevis + if util.IsTrue(d.Luks.Cex.Enabled) { + if d.Layout == nil { + r.AddOnError(c.Append("luks", "cex"), common.ErrCexArchitectureMismatch) + } else if !strings.HasPrefix(*d.Layout, "s390x") { + r.AddOnError(c.Append("layout"), common.ErrCexArchitectureMismatch) + } + if len(d.Luks.Tang) > 0 || util.IsTrue(d.Luks.Tpm2) { + r.AddOnError(c.Append("luks"), errors.ErrCexWithClevis) + } + } + + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (l BootDeviceLuks) Validate(c path.ContextPath) (r report.Report) { + if util.NotEmpty(l.Device) { + valid := false + for _, t := range l.Tang { + if t != (base.Tang{}) { + valid = true + } + } + if util.IsTrue(l.Tpm2) { + valid = true + } else if util.IsTrue(l.Cex.Enabled) { + valid = true + } + if !valid { + r.AddOnError(c.Append("luks"), common.ErrNoLuksMethodSpecified) + } + } + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} + +func (user GrubUser) Validate(c path.ContextPath) (r report.Report) { + if user.Name == "" { + r.AddOnError(c.Append("name"), common.ErrGrubUserNameNotSpecified) + } + + if !util.NotEmpty(user.PasswordHash) { + r.AddOnError(c.Append("password_hash"), common.ErrGrubPasswordNotSpecified) + } + return +} diff --git a/config/fcos/v1_8_exp/validate_test.go b/config/fcos/v1_8_exp/validate_test.go new file mode 100644 index 000000000..e710baa2e --- /dev/null +++ b/config/fcos/v1_8_exp/validate_test.go @@ -0,0 +1,627 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v1_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 6, + }, + // Ignition validation error, partition list + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 6, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_7Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // complete config with cex + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + nil, + path.New("yaml"), + }, + // can not use both cex & tang + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use both cex & tpm2 + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tpm2: util.BoolToPtr(true), + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use cex on non s390x + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "layout"), + }, + // must set s390x layout with cex + { + BootDevice{ + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "luks", "cex"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayout, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + // s390x-eckd/s390x-zfcp layouts require a boot device with luks + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + }, + common.ErrNoLuksBootDevice, + path.New("yaml", "layout"), + }, + // s390x-eckd/s390x-zfcp layouts do not support mirroring + { + BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{ + "/dev/sda", + "/dev/sdb", + }, + }, + }, + common.ErrMirrorNotSupport, + path.New("yaml", "layout"), + }, + // s390x-eckd devices must start with /dev/dasd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // s390x-zfcp devices must start with /dev/sd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasd"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // mirror with layout should succeed + { + BootDevice{ + Layout: util.StrToPtr("aarch64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // mirror without layout + { + BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + common.ErrMirrorRequiresLayout, + path.New("yaml", "mirror"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad validation report") + }) + } +} + +func TestValidateGrubUser(t *testing.T) { + tests := []struct { + in GrubUser + out error + errPath path.ContextPath + }{ + // valid user + { + in: GrubUser{ + Name: "name", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: nil, + errPath: path.New("yaml"), + }, + // username is not specified + { + in: GrubUser{ + Name: "", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: common.ErrGrubUserNameNotSpecified, + errPath: path.New("yaml", "name"), + }, + // password is not specified + { + in: GrubUser{ + Name: "name", + }, + out: common.ErrGrubPasswordNotSpecified, + errPath: path.New("yaml", "password_hash"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateMountPoints(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (has prefix "/etc" or "/var") + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/etc/foo"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/var"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/invalid/path"), + WithMountUnit: util.BoolToPtr(false), + }, + { + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: nil, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + // invalid config (path name is '/') + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /boot) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/boot"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is invalid, does not contain /etc or /var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/thisIsABugTest"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /varnish) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/varnish"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /foo/var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/foo/var"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "invalid report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (wipe_table is true) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(true), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is /dev/disk/by-id/coreos-boot-disk) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: rootDevice, + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + }, + }, + }, + // invalid config (wipe_table is nil) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + // invalid config (wipe_table is false with a partition numbered 0) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + { + Label: util.StrToPtr("bar"), + Number: 2, + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "invalid report") + }) + } +} diff --git a/config/fiot/v1_1_exp/schema.go b/config/fiot/v1_1_exp/schema.go index 20868d01f..e60659cdf 100644 --- a/config/fiot/v1_1_exp/schema.go +++ b/config/fiot/v1_1_exp/schema.go @@ -15,7 +15,7 @@ package v1_1_exp import ( - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" ) type Config struct { diff --git a/config/fiot/v1_1_exp/translate.go b/config/fiot/v1_1_exp/translate.go index e3f9d2fb2..c884709c2 100644 --- a/config/fiot/v1_1_exp/translate.go +++ b/config/fiot/v1_1_exp/translate.go @@ -18,7 +18,7 @@ import ( "github.com/coreos/butane/config/common" cutil "github.com/coreos/butane/config/util" - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/report" ) @@ -37,18 +37,18 @@ func (c Config) FieldFilters() *cutil.FieldFilters { return &fieldFilters } -// ToIgn3_6 translates the config to an Ignition config. It returns a +// ToIgn3_7 translates the config to an Ignition config. It returns a // report of any errors or warnings in the source and resultant config. If // the report has fatal errors or it encounters other problems translating, // an error is returned. -func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) +func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) return cfg.(types.Config), r, err } -// ToIgn3_6Bytes translates from a v1.2 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or +// ToIgn3_7Bytes translates from a v1.2 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or // warnings in the source and resultant config. If the report has fatal errors or it encounters other problems // translating, an error is returned. -func ToIgn3_6Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { - return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) +func ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) } diff --git a/config/fiot/v1_1_exp/translate_test.go b/config/fiot/v1_1_exp/translate_test.go index fc1d2a234..bafd6d4de 100644 --- a/config/fiot/v1_1_exp/translate_test.go +++ b/config/fiot/v1_1_exp/translate_test.go @@ -19,7 +19,7 @@ import ( "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" confutil "github.com/coreos/butane/config/util" "github.com/coreos/ignition/v2/config/util" @@ -169,7 +169,7 @@ func TestTranslateInvalid(t *testing.T) { for _, entry := range test.Entries { expectedReport.AddOnError(entry.Path, entry.Err) } - actual, translations, r := test.In.ToIgn3_6Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.In.ToIgn3_7Unvalidated(common.TranslateOptions{}) r.Merge(fieldFilters.Verify(actual)) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.In, r) diff --git a/config/flatcar/v1_2_exp/schema.go b/config/flatcar/v1_2_exp/schema.go index 55da8b63b..bef41f95e 100644 --- a/config/flatcar/v1_2_exp/schema.go +++ b/config/flatcar/v1_2_exp/schema.go @@ -15,7 +15,7 @@ package v1_2_exp import ( - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" ) type Config struct { diff --git a/config/flatcar/v1_2_exp/translate.go b/config/flatcar/v1_2_exp/translate.go index eb3522036..f219a88e5 100644 --- a/config/flatcar/v1_2_exp/translate.go +++ b/config/flatcar/v1_2_exp/translate.go @@ -18,7 +18,7 @@ import ( "github.com/coreos/butane/config/common" cutil "github.com/coreos/butane/config/util" - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/report" ) @@ -37,14 +37,14 @@ func (c Config) FieldFilters() *cutil.FieldFilters { // report of any errors or warnings in the source and resultant config. If // the report has fatal errors or it encounters other problems translating, // an error is returned. -func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) +func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) return cfg.(types.Config), r, err } // ToIgn3_5Bytes translates from a v1.2 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or // warnings in the source and resultant config. If the report has fatal errors or it encounters other problems // translating, an error is returned. -func ToIgn3_6Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { - return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) +func ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) } diff --git a/config/flatcar/v1_2_exp/translate_test.go b/config/flatcar/v1_2_exp/translate_test.go index d0f4b5aaf..086a165df 100644 --- a/config/flatcar/v1_2_exp/translate_test.go +++ b/config/flatcar/v1_2_exp/translate_test.go @@ -19,7 +19,7 @@ import ( "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" confutil "github.com/coreos/butane/config/util" @@ -69,7 +69,7 @@ func TestTranslation(t *testing.T) { for _, entry := range test.entries { expectedReport.AddOn(entry.path, entry.err, entry.kind) } - actual, translations, r := test.in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) if test.in.FieldFilters() != nil { r.Merge(test.in.FieldFilters().Verify(actual)) } diff --git a/config/openshift/v4_22_exp/result/schema.go b/config/openshift/v4_22_exp/result/schema.go index c38ee02e7..91f506919 100644 --- a/config/openshift/v4_22_exp/result/schema.go +++ b/config/openshift/v4_22_exp/result/schema.go @@ -15,7 +15,7 @@ package result import ( - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" ) const ( diff --git a/config/openshift/v4_22_exp/schema.go b/config/openshift/v4_22_exp/schema.go index 805f896f7..bc94cd926 100644 --- a/config/openshift/v4_22_exp/schema.go +++ b/config/openshift/v4_22_exp/schema.go @@ -15,7 +15,7 @@ package v4_22_exp import ( - fcos "github.com/coreos/butane/config/fcos/v1_7_exp" + fcos "github.com/coreos/butane/config/fcos/v1_8_exp" ) const ROLE_LABEL_KEY = "machineconfiguration.openshift.io/role" diff --git a/config/openshift/v4_22_exp/translate.go b/config/openshift/v4_22_exp/translate.go index 47346af0a..15100f0cc 100644 --- a/config/openshift/v4_22_exp/translate.go +++ b/config/openshift/v4_22_exp/translate.go @@ -22,7 +22,7 @@ import ( cutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" ) @@ -104,7 +104,7 @@ func (c Config) FieldFilters() *cutil.FieldFilters { // can be tracked back to their source in the source config. No config // validation is performed on input or output. func (c Config) ToMachineConfig4_22Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { - cfg, ts, r := c.Config.ToIgn3_6Unvalidated(options) + cfg, ts, r := c.Config.ToIgn3_7Unvalidated(options) if r.IsFatal() { return result.MachineConfig{}, ts, r } @@ -162,11 +162,11 @@ func (c Config) ToMachineConfig4_22(options common.TranslateOptions) (result.Mac return cfg.(result.MachineConfig), r, err } -// ToIgn3_6Unvalidated translates the config to an Ignition config. It also +// ToIgn3_7Unvalidated translates the config to an Ignition config. It also // returns the set of translations it did so paths in the resultant config // can be tracked back to their source in the source config. No config // validation is performed on input or output. -func (c Config) ToIgn3_6Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { +func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { mc, ts, r := c.ToMachineConfig4_22Unvalidated(options) cfg := mc.Spec.Config @@ -182,12 +182,12 @@ func (c Config) ToIgn3_6Unvalidated(options common.TranslateOptions) (types.Conf return cfg, ts, r } -// ToIgn3_6 translates the config to an Ignition config. It returns a +// ToIgn3_7 translates the config to an Ignition config. It returns a // report of any errors or warnings in the source and resultant config. If // the report has fatal errors or it encounters other problems translating, // an error is returned. -func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) +func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) return cfg.(types.Config), r, err } @@ -196,7 +196,7 @@ func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report. // translating, an error is returned. func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { if options.Raw { - return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) } else { return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_22", options) } diff --git a/config/openshift/v4_22_exp/translate_test.go b/config/openshift/v4_22_exp/translate_test.go index 28043481f..d0f709d98 100644 --- a/config/openshift/v4_22_exp/translate_test.go +++ b/config/openshift/v4_22_exp/translate_test.go @@ -19,15 +19,15 @@ import ( "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" - fcos "github.com/coreos/butane/config/fcos/v1_7_exp" + fcos "github.com/coreos/butane/config/fcos/v1_8_exp" "github.com/coreos/butane/config/openshift/v4_22_exp/result" confutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" "github.com/coreos/ignition/v2/config/util" - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/path" "github.com/coreos/vcontext/report" "github.com/stretchr/testify/assert" @@ -52,7 +52,7 @@ func TestElidedFieldWarning(t *testing.T) { expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) - _, _, r := in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + _, _, r := in.ToIgn3_7Unvalidated(common.TranslateOptions{}) assert.Equal(t, expected, r, "report mismatch") } @@ -84,7 +84,7 @@ func TestTranslateConfig(t *testing.T) { Spec: result.Spec{ Config: types.Config{ Ignition: types.Ignition{ - Version: "3.6.0", + Version: "3.7.0-experimental", }, }, }, @@ -130,7 +130,7 @@ func TestTranslateConfig(t *testing.T) { Spec: result.Spec{ Config: types.Config{ Ignition: types.Ignition{ - Version: "3.6.0", + Version: "3.7.0-experimental", }, Storage: types.Storage{ Filesystems: []types.Filesystem{ diff --git a/config/openshift/v4_22_exp/validate_test.go b/config/openshift/v4_22_exp/validate_test.go index 55233644f..ebdbefe8b 100644 --- a/config/openshift/v4_22_exp/validate_test.go +++ b/config/openshift/v4_22_exp/validate_test.go @@ -19,9 +19,9 @@ import ( "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" - fcos "github.com/coreos/butane/config/fcos/v1_7_exp" + fcos "github.com/coreos/butane/config/fcos/v1_8_exp" "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/util" diff --git a/config/r4e/v1_2_exp/schema.go b/config/r4e/v1_2_exp/schema.go index 56eea4787..52726e210 100644 --- a/config/r4e/v1_2_exp/schema.go +++ b/config/r4e/v1_2_exp/schema.go @@ -15,7 +15,7 @@ package v1_2_exp import ( - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" ) type Config struct { diff --git a/config/r4e/v1_2_exp/translate.go b/config/r4e/v1_2_exp/translate.go index f0d835974..fc8c38214 100644 --- a/config/r4e/v1_2_exp/translate.go +++ b/config/r4e/v1_2_exp/translate.go @@ -18,7 +18,7 @@ import ( "github.com/coreos/butane/config/common" cutil "github.com/coreos/butane/config/util" - "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" "github.com/coreos/vcontext/report" ) @@ -37,18 +37,18 @@ func (c Config) FieldFilters() *cutil.FieldFilters { return &fieldFilters } -// ToIgn3_6 translates the config to an Ignition config. It returns a +// ToIgn3_7 translates the config to an Ignition config. It returns a // report of any errors or warnings in the source and resultant config. If // the report has fatal errors or it encounters other problems translating, // an error is returned. -func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) +func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) return cfg.(types.Config), r, err } -// ToIgn3_6Bytes translates from a v1.2 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or +// ToIgn3_7Bytes translates from a v1.2 Butane config to a v3.5.0 Ignition config. It returns a report of any errors or // warnings in the source and resultant config. If the report has fatal errors or it encounters other problems // translating, an error is returned. -func ToIgn3_6Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { - return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) +func ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) } diff --git a/config/r4e/v1_2_exp/translate_test.go b/config/r4e/v1_2_exp/translate_test.go index efd656be6..f81f21dc9 100644 --- a/config/r4e/v1_2_exp/translate_test.go +++ b/config/r4e/v1_2_exp/translate_test.go @@ -19,7 +19,7 @@ import ( "testing" baseutil "github.com/coreos/butane/base/util" - base "github.com/coreos/butane/base/v0_7_exp" + base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" confutil "github.com/coreos/butane/config/util" "github.com/coreos/ignition/v2/config/util" @@ -169,7 +169,7 @@ func TestTranslateInvalid(t *testing.T) { for _, entry := range test.Entries { expectedReport.AddOnError(entry.Path, entry.Err) } - actual, translations, r := test.In.ToIgn3_6Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.In.ToIgn3_7Unvalidated(common.TranslateOptions{}) r.Merge(fieldFilters.Verify(actual)) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.In, r) diff --git a/docs/config-fcos-v1_7.md b/docs/config-fcos-v1_7.md new file mode 100644 index 000000000..433092298 --- /dev/null +++ b/docs/config-fcos-v1_7.md @@ -0,0 +1,237 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.7.0 +parent: Configuration specifications +nav_order: 42 +--- + +# Fedora CoreOS Specification v1.7.0 + +The Fedora CoreOS configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `fcos` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.7.0` and generates Ignition configs with version `3.6.0`. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `