func Parse(data []byte) (types.Config, report.Report) { var cfg types.Config var r report.Report if err := yaml.Unmarshal(data, &cfg); err != nil { return types.Config{}, report.ReportFromError(err, report.EntryError) } nodes := yaml.UnmarshalToNode(data) if nodes == nil { r.Add(report.Entry{ Kind: report.EntryWarning, Message: "Configuration is empty", }) r.Merge(validate.ValidateWithoutSource(reflect.ValueOf(cfg))) } else { root, err := fromYamlDocumentNode(*nodes) if err != nil { return types.Config{}, report.ReportFromError(err, report.EntryError) } r.Merge(validate.Validate(reflect.ValueOf(cfg), root, nil)) } if r.IsFatal() { return types.Config{}, r } return cfg, r }
func (f Filesystem) Validate() report.Report { if f.Mount == nil && f.Path == nil { return report.ReportFromError(ErrFilesystemNoMountPath, report.EntryError) } if f.Mount != nil && f.Path != nil { return report.ReportFromError(ErrFilesystemMountAndPath, report.EntryError) } return report.Report{} }
func (v IgnitionVersion) Validate() report.Report { if MaxVersion.Major > v.Major { return report.ReportFromError(ErrOldVersion, report.EntryError) } if MaxVersion.LessThan(semver.Version(v)) { return report.ReportFromError(ErrNewVersion, report.EntryError) } return report.Report{} }
func (d PartitionTypeGUID) Validate() report.Report { ok, err := regexp.MatchString("^(|[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})$", string(d)) if err != nil { return report.ReportFromError(fmt.Errorf("error matching type-guid regexp: %v", err), report.EntryError) } if !ok { return report.ReportFromError(fmt.Errorf(`partition type-guid must have the form "01234567-89AB-CDEF-EDCB-A98765432101", got: %q`, string(d)), report.EntryError) } return report.Report{} }
// Parse parses the raw config into a types.Config struct and generates a report of any // errors, warnings, info, and deprecations it encountered func Parse(rawConfig []byte) (types.Config, report.Report, error) { switch majorVersion(rawConfig) { case 1: config, err := ParseFromV1(rawConfig) if err != nil { return types.Config{}, report.ReportFromError(err, report.EntryError), err } return config, report.ReportFromError(ErrDeprecated, report.EntryDeprecated), nil default: return ParseFromLatest(rawConfig) } }
func (h Hash) Validate() report.Report { var hash crypto.Hash switch h.Function { case "sha512": hash = crypto.SHA512 default: return report.ReportFromError(ErrHashUnrecognized, report.EntryError) } if len(h.Sum) != hex.EncodedLen(hash.Size()) { return report.ReportFromError(ErrHashWrongSize, report.EntryError) } return report.Report{} }
func TestFilesystemFormatValidate(t *testing.T) { type in struct { format FilesystemFormat } type out struct { err error } tests := []struct { in in out out }{ { in: in{format: FilesystemFormat("ext4")}, out: out{}, }, { in: in{format: FilesystemFormat("btrfs")}, out: out{}, }, { in: in{format: FilesystemFormat("")}, out: out{err: ErrFilesystemInvalidFormat}, }, } for i, test := range tests { err := test.in.format.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func (n Raid) Validate() report.Report { switch n.Level { case "linear", "raid0", "0", "stripe": if n.Spares != 0 { return report.ReportFromError(fmt.Errorf("spares unsupported for %q arrays", n.Level), report.EntryError) } case "raid1", "1", "mirror": case "raid4", "4": case "raid5", "5": case "raid6", "6": case "raid10", "10": default: return report.ReportFromError(fmt.Errorf("unrecognized raid level: %q", n.Level), report.EntryError) } return report.Report{} }
func TestSystemdUnitNameValidate(t *testing.T) { type in struct { unit SystemdUnitName } type out struct { err error } tests := []struct { in in out out }{ { in: in{unit: SystemdUnitName("test.service")}, out: out{err: nil}, }, { in: in{unit: SystemdUnitName("test.socket")}, out: out{err: nil}, }, { in: in{unit: SystemdUnitName("test.blah")}, out: out{err: errors.New("invalid systemd unit extension")}, }, } for i, test := range tests { err := test.in.unit.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func (n SystemdUnitDropInName) Validate() report.Report { switch filepath.Ext(string(n)) { case ".conf": return report.Report{} default: return report.ReportFromError(errors.New("invalid systemd unit drop-in extension"), report.EntryError) } }
func (n SystemdUnitName) Validate() report.Report { switch filepath.Ext(string(n)) { case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope": return report.Report{} default: return report.ReportFromError(errors.New("invalid systemd unit extension"), report.EntryError) } }
func (u Url) Validate() report.Report { // Empty url is valid, indicates an empty file if u.String() == "" { return report.Report{} } switch url.URL(u).Scheme { case "http", "https", "oem": return report.Report{} case "data": if _, err := dataurl.DecodeString(u.String()); err != nil { return report.ReportFromError(err, report.EntryError) } return report.Report{} default: return report.ReportFromError(ErrInvalidScheme, report.EntryError) } }
func (c Compression) Validate() report.Report { switch c { case "", "gzip": default: return report.ReportFromError(ErrCompressionInvalid, report.EntryError) } return report.Report{} }
func (f FilesystemFormat) Validate() report.Report { switch f { case "ext4", "btrfs", "xfs": return report.Report{} default: return report.ReportFromError(ErrFilesystemInvalidFormat, report.EntryError) } }
func (n NetworkdUnitName) Validate() report.Report { switch filepath.Ext(string(n)) { case ".link", ".netdev", ".network": return report.Report{} default: return report.ReportFromError(errors.New("invalid networkd unit extension"), report.EntryError) } }
func (n PartitionLabel) Validate() report.Report { // http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries: // 56 (0x38) 72 bytes Partition name (36 UTF-16LE code units) // XXX(vc): note GPT calls it a name, we're using label for consistency // with udev naming /dev/disk/by-partlabel/*. if len(string(n)) > 36 { return report.ReportFromError(fmt.Errorf("partition labels may not exceed 36 characters"), report.EntryError) } return report.Report{} }
func (u Url) Validate() report.Report { // Empty url is valid, indicates an empty file if u.String() == "" { return report.Report{} } switch url.URL(u).Scheme { case "http", "https", "oem", "data": return report.Report{} } return report.ReportFromError(ErrInvalidScheme, report.EntryError) }
func TestURLValidate(t *testing.T) { type in struct { u string } type out struct { err error } tests := []struct { in in out out }{ { in: in{u: ""}, out: out{}, }, { in: in{u: "http://example.com"}, out: out{}, }, { in: in{u: "https://example.com"}, out: out{}, }, { in: in{u: "oem:///foobar"}, out: out{}, }, { in: in{u: "data:,example%20file%0A"}, out: out{}, }, { in: in{u: "bad://"}, out: out{err: ErrInvalidScheme}, }, } for i, test := range tests { u, err := url.Parse(test.in.u) if err != nil { t.Errorf("URL failed to parse. This is an error with the test") } r := Url(*u).Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), r) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, r) } } }
func TestFileValidate(t *testing.T) { type in struct { mode FileMode } type out struct { err error } tests := []struct { in in out out }{ { in: in{mode: FileMode(0)}, out: out{}, }, { in: in{mode: FileMode(0644)}, out: out{}, }, { in: in{mode: FileMode(01755)}, out: out{}, }, { in: in{mode: FileMode(07777)}, out: out{}, }, { in: in{mode: FileMode(010000)}, out: out{err: ErrFileIllegalMode}, }, } for i, test := range tests { err := test.in.mode.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func TestPathValidate(t *testing.T) { type in struct { device Path } type out struct { err error } tests := []struct { in in out out }{ { in: in{device: Path("/good/path")}, out: out{}, }, { in: in{device: Path("/name")}, out: out{}, }, { in: in{device: Path("/this/is/a/fairly/long/path/to/a/device.")}, out: out{}, }, { in: in{device: Path("/this one has spaces")}, out: out{}, }, { in: in{device: Path("relative/path")}, out: out{err: ErrPathRelative}, }, } for i, test := range tests { err := test.in.device.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func TestHashValidate(t *testing.T) { type in struct { hash Hash } type out struct { err error } tests := []struct { in in out out }{ { in: in{hash: Hash{}}, out: out{err: ErrHashUnrecognized}, }, { in: in{hash: Hash{Function: "xor"}}, out: out{err: ErrHashUnrecognized}, }, { in: in{hash: Hash{Function: "sha512", Sum: "123"}}, out: out{err: ErrHashWrongSize}, }, { in: in{hash: Hash{Function: "sha512", Sum: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}, out: out{}, }, } for i, test := range tests { err := test.in.hash.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func TestFilesystemValidate(t *testing.T) { type in struct { filesystem Filesystem } type out struct { err error } tests := []struct { in in out out }{ { in: in{filesystem: Filesystem{Mount: &FilesystemMount{Device: "/foo", Format: "ext4"}}}, out: out{}, }, { in: in{filesystem: Filesystem{Path: func(p Path) *Path { return &p }("/mount")}}, out: out{}, }, { in: in{filesystem: Filesystem{Path: func(p Path) *Path { return &p }("/mount"), Mount: &FilesystemMount{Device: "/foo", Format: "ext4"}}}, out: out{err: ErrFilesystemMountAndPath}, }, { in: in{filesystem: Filesystem{}}, out: out{err: ErrFilesystemNoMountPath}, }, } for i, test := range tests { err := test.in.filesystem.Validate() if !reflect.DeepEqual(report.ReportFromError(test.out.err, report.EntryError), err) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) } } }
func (p Path) Validate() report.Report { if !filepath.IsAbs(string(p)) { return report.ReportFromError(ErrPathRelative, report.EntryError) } return report.Report{} }
func (f File) Validate() report.Report { if f.Filesystem == "" { return report.ReportFromError(ErrNoFilesystem, report.EntryError) } return report.Report{} }
func (m FileMode) Validate() report.Report { if (m &^ 07777) != 0 { return report.ReportFromError(ErrFileIllegalMode, report.EntryError) } return report.Report{} }
func ConvertAs2_0_0(in types.Config) (ignTypes.Config, report.Report) { out := ignTypes.Config{ Ignition: ignTypes.Ignition{ Version: ignTypes.IgnitionVersion{Major: 2, Minor: 0}, }, } for _, ref := range in.Ignition.Config.Append { newRef, err := convertConfigReference(ref) if err != nil { return ignTypes.Config{}, report.ReportFromError(err, report.EntryError) } out.Ignition.Config.Append = append(out.Ignition.Config.Append, newRef) } if in.Ignition.Config.Replace != nil { newRef, err := convertConfigReference(*in.Ignition.Config.Replace) if err != nil { return ignTypes.Config{}, report.ReportFromError(err, report.EntryError) } out.Ignition.Config.Replace = &newRef } for _, disk := range in.Storage.Disks { newDisk := ignTypes.Disk{ Device: ignTypes.Path(disk.Device), WipeTable: disk.WipeTable, } for _, partition := range disk.Partitions { size, err := convertPartitionDimension(partition.Size) if err != nil { return ignTypes.Config{}, report.ReportFromError(err, report.EntryError) } start, err := convertPartitionDimension(partition.Start) if err != nil { return ignTypes.Config{}, report.ReportFromError(err, report.EntryError) } newDisk.Partitions = append(newDisk.Partitions, ignTypes.Partition{ Label: ignTypes.PartitionLabel(partition.Label), Number: partition.Number, Size: size, Start: start, TypeGUID: ignTypes.PartitionTypeGUID(partition.TypeGUID), }) } out.Storage.Disks = append(out.Storage.Disks, newDisk) } for _, array := range in.Storage.Arrays { newArray := ignTypes.Raid{ Name: array.Name, Level: array.Level, Spares: array.Spares, } for _, device := range array.Devices { newArray.Devices = append(newArray.Devices, ignTypes.Path(device)) } out.Storage.Arrays = append(out.Storage.Arrays, newArray) } for _, filesystem := range in.Storage.Filesystems { newFilesystem := ignTypes.Filesystem{ Name: filesystem.Name, Path: func(p ignTypes.Path) *ignTypes.Path { if p == "" { return nil } return &p }(ignTypes.Path(filesystem.Path)), } if filesystem.Mount != nil { newFilesystem.Mount = &ignTypes.FilesystemMount{ Device: ignTypes.Path(filesystem.Mount.Device), Format: ignTypes.FilesystemFormat(filesystem.Mount.Format), } if filesystem.Mount.Create != nil { newFilesystem.Mount.Create = &ignTypes.FilesystemCreate{ Force: filesystem.Mount.Create.Force, Options: ignTypes.MkfsOptions(filesystem.Mount.Create.Options), } } } out.Storage.Filesystems = append(out.Storage.Filesystems, newFilesystem) } for _, file := range in.Storage.Files { newFile := ignTypes.File{ Filesystem: file.Filesystem, Path: ignTypes.Path(file.Path), Mode: ignTypes.FileMode(file.Mode), User: ignTypes.FileUser{Id: file.User.Id}, Group: ignTypes.FileGroup{Id: file.Group.Id}, } if file.Contents.Inline != "" { newFile.Contents = ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "data", Opaque: "," + dataurl.EscapeString(file.Contents.Inline), }, } } if file.Contents.Remote.Url != "" { source, err := url.Parse(file.Contents.Remote.Url) if err != nil { return ignTypes.Config{}, report.ReportFromError(err, report.EntryError) } newFile.Contents = ignTypes.FileContents{Source: ignTypes.Url(*source)} } if newFile.Contents == (ignTypes.FileContents{}) { newFile.Contents = ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "data", Opaque: ",", }, } } newFile.Contents.Compression = ignTypes.Compression(file.Contents.Remote.Compression) newFile.Contents.Verification = convertVerification(file.Contents.Remote.Verification) out.Storage.Files = append(out.Storage.Files, newFile) } for _, unit := range in.Systemd.Units { newUnit := ignTypes.SystemdUnit{ Name: ignTypes.SystemdUnitName(unit.Name), Enable: unit.Enable, Mask: unit.Mask, Contents: unit.Contents, } for _, dropIn := range unit.DropIns { newUnit.DropIns = append(newUnit.DropIns, ignTypes.SystemdUnitDropIn{ Name: ignTypes.SystemdUnitDropInName(dropIn.Name), Contents: dropIn.Contents, }) } out.Systemd.Units = append(out.Systemd.Units, newUnit) } for _, unit := range in.Networkd.Units { out.Networkd.Units = append(out.Networkd.Units, ignTypes.NetworkdUnit{ Name: ignTypes.NetworkdUnitName(unit.Name), Contents: unit.Contents, }) } for _, user := range in.Passwd.Users { newUser := ignTypes.User{ Name: user.Name, PasswordHash: user.PasswordHash, SSHAuthorizedKeys: user.SSHAuthorizedKeys, } if user.Create != nil { newUser.Create = &ignTypes.UserCreate{ Uid: user.Create.Uid, GECOS: user.Create.GECOS, Homedir: user.Create.Homedir, NoCreateHome: user.Create.NoCreateHome, PrimaryGroup: user.Create.PrimaryGroup, Groups: user.Create.Groups, NoUserGroup: user.Create.NoUserGroup, System: user.Create.System, NoLogInit: user.Create.NoLogInit, Shell: user.Create.Shell, } } out.Passwd.Users = append(out.Passwd.Users, newUser) } for _, group := range in.Passwd.Groups { out.Passwd.Groups = append(out.Passwd.Groups, ignTypes.Group{ Name: group.Name, Gid: group.Gid, PasswordHash: group.PasswordHash, System: group.System, }) } r := validate.ValidateWithoutSource(reflect.ValueOf(out)) if r.IsFatal() { return ignTypes.Config{}, r } return out, r }
func TestConvertAs2_0_0(t *testing.T) { type in struct { cfg types.Config } type out struct { cfg ignTypes.Config r report.Report } tests := []struct { in in out out }{ { in: in{cfg: types.Config{}}, out: out{cfg: ignTypes.Config{Ignition: ignTypes.Ignition{Version: ignTypes.IgnitionVersion{Major: 2}}}}, }, { in: in{cfg: types.Config{ Networkd: types.Networkd{ Units: []types.NetworkdUnit{ {Name: "bad.blah", Contents: "not valid"}, }, }, }}, out: out{r: report.ReportFromError(errors.New("invalid networkd unit extension"), report.EntryError)}, }, // Config { in: in{cfg: types.Config{ Ignition: types.Ignition{ Config: types.IgnitionConfig{ Append: []types.ConfigReference{ { Source: "http://example.com/test1", Verification: types.Verification{ Hash: types.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, { Source: "http://example.com/test2", }, }, Replace: &types.ConfigReference{ Source: "http://example.com/test3", Verification: types.Verification{ Hash: types.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, }, }, }}, out: out{cfg: ignTypes.Config{ Ignition: ignTypes.Ignition{ Version: ignTypes.IgnitionVersion{Major: 2}, Config: ignTypes.IgnitionConfig{ Append: []ignTypes.ConfigReference{ { Source: ignTypes.Url{ Scheme: "http", Host: "example.com", Path: "/test1", }, Verification: ignTypes.Verification{ Hash: &ignTypes.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, { Source: ignTypes.Url{ Scheme: "http", Host: "example.com", Path: "/test2", }, }, }, Replace: &ignTypes.ConfigReference{ Source: ignTypes.Url{ Scheme: "http", Host: "example.com", Path: "/test3", }, Verification: ignTypes.Verification{ Hash: &ignTypes.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, }, }, }}, }, // Storage { in: in{cfg: types.Config{ Storage: types.Storage{ Disks: []types.Disk{ { Device: "/dev/sda", WipeTable: true, Partitions: []types.Partition{ { Label: "ROOT", Number: 7, Size: "100MB", Start: "50MB", TypeGUID: "11111111-1111-1111-1111-111111111111", }, { Label: "DATA", Number: 12, Size: "1GB", Start: "300MB", TypeGUID: "00000000-0000-0000-0000-000000000000", }, { Label: "NOTHING", }, }, }, { Device: "/dev/sdb", WipeTable: true, }, }, Arrays: []types.Raid{ { Name: "fast", Level: "raid0", Devices: []string{"/dev/sdc", "/dev/sdd"}, }, { Name: "durable", Level: "raid1", Devices: []string{"/dev/sde", "/dev/sdf", "/dev/sdg"}, Spares: 1, }, }, Filesystems: []types.Filesystem{ { Name: "filesystem1", Mount: &types.Mount{ Device: "/dev/disk/by-partlabel/ROOT", Format: "btrfs", Create: &types.Create{ Force: true, Options: []string{"-L", "ROOT"}, }, }, }, { Name: "filesystem2", Mount: &types.Mount{ Device: "/dev/disk/by-partlabel/DATA", Format: "ext4", }, }, { Name: "filesystem3", Path: "/sysroot", }, }, Files: []types.File{ { Filesystem: "filesystem1", Path: "/opt/file1", Contents: types.FileContents{ Inline: "file1", }, Mode: 0644, User: types.FileUser{Id: 500}, Group: types.FileGroup{Id: 501}, }, { Filesystem: "filesystem1", Path: "/opt/file2", Contents: types.FileContents{ Remote: types.Remote{ Url: "http://example.com/file2", Compression: "gzip", Verification: types.Verification{ Hash: types.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, }, Mode: 0644, User: types.FileUser{Id: 502}, Group: types.FileGroup{Id: 503}, }, { Filesystem: "filesystem2", Path: "/opt/file3", Contents: types.FileContents{ Remote: types.Remote{ Url: "http://example.com/file3", Compression: "gzip", }, }, Mode: 0400, User: types.FileUser{Id: 1000}, Group: types.FileGroup{Id: 1001}, }, { Filesystem: "filesystem2", Path: "/opt/file4", Contents: types.FileContents{ Inline: "", }, }, }, }, }}, out: out{cfg: ignTypes.Config{ Ignition: ignTypes.Ignition{Version: ignTypes.IgnitionVersion{Major: 2}}, Storage: ignTypes.Storage{ Disks: []ignTypes.Disk{ { Device: ignTypes.Path("/dev/sda"), WipeTable: true, Partitions: []ignTypes.Partition{ { Label: ignTypes.PartitionLabel("ROOT"), Number: 7, Size: ignTypes.PartitionDimension(0x32000), Start: ignTypes.PartitionDimension(0x19000), TypeGUID: "11111111-1111-1111-1111-111111111111", }, { Label: ignTypes.PartitionLabel("DATA"), Number: 12, Size: ignTypes.PartitionDimension(0x200000), Start: ignTypes.PartitionDimension(0x96000), TypeGUID: "00000000-0000-0000-0000-000000000000", }, { Label: ignTypes.PartitionLabel("NOTHING"), }, }, }, { Device: ignTypes.Path("/dev/sdb"), WipeTable: true, }, }, Arrays: []ignTypes.Raid{ { Name: "fast", Level: "raid0", Devices: []ignTypes.Path{ignTypes.Path("/dev/sdc"), ignTypes.Path("/dev/sdd")}, }, { Name: "durable", Level: "raid1", Devices: []ignTypes.Path{ignTypes.Path("/dev/sde"), ignTypes.Path("/dev/sdf"), ignTypes.Path("/dev/sdg")}, Spares: 1, }, }, Filesystems: []ignTypes.Filesystem{ { Name: "filesystem1", Mount: &ignTypes.FilesystemMount{ Device: ignTypes.Path("/dev/disk/by-partlabel/ROOT"), Format: ignTypes.FilesystemFormat("btrfs"), Create: &ignTypes.FilesystemCreate{ Force: true, Options: ignTypes.MkfsOptions([]string{"-L", "ROOT"}), }, }, }, { Name: "filesystem2", Mount: &ignTypes.FilesystemMount{ Device: ignTypes.Path("/dev/disk/by-partlabel/DATA"), Format: ignTypes.FilesystemFormat("ext4"), }, }, { Name: "filesystem3", Path: func(p ignTypes.Path) *ignTypes.Path { return &p }("/sysroot"), }, }, Files: []ignTypes.File{ { Filesystem: "filesystem1", Path: ignTypes.Path("/opt/file1"), Contents: ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "data", Opaque: ",file1", }, }, Mode: ignTypes.FileMode(0644), User: ignTypes.FileUser{Id: 500}, Group: ignTypes.FileGroup{Id: 501}, }, { Filesystem: "filesystem1", Path: ignTypes.Path("/opt/file2"), Contents: ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "http", Host: "example.com", Path: "/file2", }, Compression: "gzip", Verification: ignTypes.Verification{ Hash: &ignTypes.Hash{ Function: "sha512", Sum: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }, }, Mode: ignTypes.FileMode(0644), User: ignTypes.FileUser{Id: 502}, Group: ignTypes.FileGroup{Id: 503}, }, { Filesystem: "filesystem2", Path: ignTypes.Path("/opt/file3"), Contents: ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "http", Host: "example.com", Path: "/file3", }, Compression: "gzip", }, Mode: ignTypes.FileMode(0400), User: ignTypes.FileUser{Id: 1000}, Group: ignTypes.FileGroup{Id: 1001}, }, { Filesystem: "filesystem2", Path: ignTypes.Path("/opt/file4"), Contents: ignTypes.FileContents{ Source: ignTypes.Url{ Scheme: "data", Opaque: ",", }, }, }, }, }, }}, }, // systemd { in: in{cfg: types.Config{ Systemd: types.Systemd{ Units: []types.SystemdUnit{ { Name: "test1.service", Enable: true, Contents: "test1 contents", DropIns: []types.SystemdUnitDropIn{ { Name: "conf1.conf", Contents: "conf1 contents", }, { Name: "conf2.conf", Contents: "conf2 contents", }, }, }, { Name: "test2.service", Mask: true, Contents: "test2 contents", }, }, }, }}, out: out{cfg: ignTypes.Config{ Ignition: ignTypes.Ignition{Version: ignTypes.IgnitionVersion{Major: 2}}, Systemd: ignTypes.Systemd{ Units: []ignTypes.SystemdUnit{ { Name: "test1.service", Enable: true, Contents: "test1 contents", DropIns: []ignTypes.SystemdUnitDropIn{ { Name: "conf1.conf", Contents: "conf1 contents", }, { Name: "conf2.conf", Contents: "conf2 contents", }, }, }, { Name: "test2.service", Mask: true, Contents: "test2 contents", }, }, }, }}, }, // networkd { in: in{cfg: types.Config{ Networkd: types.Networkd{ Units: []types.NetworkdUnit{ { Name: "empty.netdev", }, { Name: "test.network", Contents: "test config", }, }, }, }}, out: out{cfg: ignTypes.Config{ Ignition: ignTypes.Ignition{Version: ignTypes.IgnitionVersion{Major: 2}}, Networkd: ignTypes.Networkd{ Units: []ignTypes.NetworkdUnit{ { Name: "empty.netdev", }, { Name: "test.network", Contents: "test config", }, }, }, }}, }, // passwd { in: in{cfg: types.Config{ Passwd: types.Passwd{ Users: []types.User{ { Name: "user 1", PasswordHash: "password 1", SSHAuthorizedKeys: []string{"key1", "key2"}, }, { Name: "user 2", PasswordHash: "password 2", SSHAuthorizedKeys: []string{"key3", "key4"}, Create: &types.UserCreate{ Uid: func(i uint) *uint { return &i }(123), GECOS: "gecos", Homedir: "/home/user 2", NoCreateHome: true, PrimaryGroup: "wheel", Groups: []string{"wheel", "plugdev"}, NoUserGroup: true, System: true, NoLogInit: true, Shell: "/bin/zsh", }, }, { Name: "user 3", PasswordHash: "password 3", SSHAuthorizedKeys: []string{"key5", "key6"}, Create: &types.UserCreate{}, }, }, Groups: []types.Group{ { Name: "group 1", Gid: func(i uint) *uint { return &i }(1000), PasswordHash: "password 1", System: true, }, { Name: "group 2", PasswordHash: "password 2", }, }, }, }}, out: out{cfg: ignTypes.Config{ Ignition: ignTypes.Ignition{Version: ignTypes.IgnitionVersion{Major: 2}}, Passwd: ignTypes.Passwd{ Users: []ignTypes.User{ { Name: "user 1", PasswordHash: "password 1", SSHAuthorizedKeys: []string{"key1", "key2"}, }, { Name: "user 2", PasswordHash: "password 2", SSHAuthorizedKeys: []string{"key3", "key4"}, Create: &ignTypes.UserCreate{ Uid: func(i uint) *uint { return &i }(123), GECOS: "gecos", Homedir: "/home/user 2", NoCreateHome: true, PrimaryGroup: "wheel", Groups: []string{"wheel", "plugdev"}, NoUserGroup: true, System: true, NoLogInit: true, Shell: "/bin/zsh", }, }, { Name: "user 3", PasswordHash: "password 3", SSHAuthorizedKeys: []string{"key5", "key6"}, Create: &ignTypes.UserCreate{}, }, }, Groups: []ignTypes.Group{ { Name: "group 1", Gid: func(i uint) *uint { return &i }(1000), PasswordHash: "password 1", System: true, }, { Name: "group 2", PasswordHash: "password 2", }, }, }, }}, }, } for i, test := range tests { cfg, r := ConvertAs2_0_0(test.in.cfg) if !reflect.DeepEqual(r, test.out.r) { t.Errorf("#%d: bad error: want %v, got %v", i, test.out.r, r) } if !reflect.DeepEqual(cfg, test.out.cfg) { t.Errorf("#%d: bad config: want %#v, got %#v", i, test.out.cfg, cfg) } } }
func ParseFromLatest(rawConfig []byte) (types.Config, report.Report, error) { if isEmpty(rawConfig) { return types.Config{}, report.Report{}, ErrEmpty } else if isCloudConfig(rawConfig) { return types.Config{}, report.Report{}, ErrCloudConfig } else if isScript(rawConfig) { return types.Config{}, report.Report{}, ErrScript } var err error var config types.Config // These errors are fatal and the config should not be further validated if err = json.Unmarshal(rawConfig, &config); err == nil { versionReport := config.Ignition.Version.Validate() if versionReport.IsFatal() { return types.Config{}, versionReport, ErrInvalid } } // Handle json syntax and type errors first, since they are fatal but have offset info if serr, ok := err.(*json.SyntaxError); ok { line, col, highlight := errorutil.HighlightBytePosition(bytes.NewReader(rawConfig), serr.Offset) return types.Config{}, report.Report{ Entries: []report.Entry{{ Kind: report.EntryError, Message: serr.Error(), Line: line, Column: col, Highlight: highlight, }}, }, ErrInvalid } if terr, ok := err.(*json.UnmarshalTypeError); ok { line, col, highlight := errorutil.HighlightBytePosition(bytes.NewReader(rawConfig), terr.Offset) return types.Config{}, report.Report{ Entries: []report.Entry{{ Kind: report.EntryError, Message: terr.Error(), Line: line, Column: col, Highlight: highlight, }}, }, ErrInvalid } // Handle other fatal errors (i.e. invalid version) if err != nil { return types.Config{}, report.ReportFromError(err, report.EntryError), err } // Unmarshal again to a json.Node to get offset information for building a report var ast json.Node var r report.Report configValue := reflect.ValueOf(config) if err := json.Unmarshal(rawConfig, &ast); err != nil { r.Add(report.Entry{ Kind: report.EntryWarning, Message: "Ignition could not unmarshal your config for reporting line numbers. This should never happen. Please file a bug.", }) r.Merge(validate.ValidateWithoutSource(configValue)) } else { r.Merge(validate.Validate(configValue, astjson.FromJsonRoot(ast), bytes.NewReader(rawConfig))) } if r.IsFatal() { return types.Config{}, r, ErrInvalid } return config, r, nil }
func TestValidate(t *testing.T) { type in struct { cfg Config } type out struct { err error } tests := []struct { in in out out }{ { in: in{cfg: Config{Ignition: Ignition{Version: IgnitionVersion{Major: 2}}}}, out: out{}, }, { in: in{cfg: Config{}}, out: out{err: ErrOldVersion}, }, { in: in{cfg: Config{ Ignition: Ignition{ Version: IgnitionVersion{Major: 2}, Config: IgnitionConfig{ Replace: &ConfigReference{ Verification: Verification{ Hash: &Hash{Function: "foobar"}, }, }, }, }, }}, out: out{errors.New("unrecognized hash function")}, }, { in: in{cfg: Config{ Ignition: Ignition{Version: IgnitionVersion{Major: 2}}, Storage: Storage{ Filesystems: []Filesystem{ { Name: "filesystem1", Mount: &FilesystemMount{ Device: Path("/dev/disk/by-partlabel/ROOT"), Format: FilesystemFormat("btrfs"), }, }, }, }, }}, out: out{}, }, { in: in{cfg: Config{ Ignition: Ignition{Version: IgnitionVersion{Major: 2}}, Storage: Storage{ Filesystems: []Filesystem{ { Name: "filesystem1", Path: func(p Path) *Path { return &p }("/sysroot"), }, }, }, }}, out: out{}, }, { in: in{cfg: Config{ Ignition: Ignition{Version: IgnitionVersion{Major: 2}}, Systemd: Systemd{Units: []SystemdUnit{{Name: "foo.bar"}}}, }}, out: out{err: errors.New("invalid systemd unit extension")}, }, } for i, test := range tests { r := ValidateWithoutSource(reflect.ValueOf(test.in.cfg)) expectedReport := report.ReportFromError(test.out.err, report.EntryError) if !reflect.DeepEqual(expectedReport, r) { t.Errorf("#%d: bad error: want %v, got %v", i, expectedReport, r) } } }