diff --git a/config/config.go b/config/config.go index 57f37df2f..5f436a8f2 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,7 @@ import ( "github.com/coreos/ignition/config/v1" "github.com/coreos/ignition/config/v2_0" "github.com/coreos/ignition/config/v2_1" + "github.com/coreos/ignition/config/v2_2" "github.com/coreos/ignition/config/validate" astjson "github.com/coreos/ignition/config/validate/astjson" "github.com/coreos/ignition/config/validate/report" @@ -71,6 +72,8 @@ func Parse(rawConfig []byte) (types.Config, report.Report, error) { return config, rpt, nil case types.MaxVersion: return ParseFromLatest(rawConfig) + case semver.Version{Major: 2, Minor: 2}: + return ParseFromV2_2(rawConfig) case semver.Version{Major: 2, Minor: 1}: return ParseFromV2_1(rawConfig) case semver.Version{Major: 2, Minor: 0}: @@ -195,6 +198,15 @@ func ParseFromV2_1(rawConfig []byte) (types.Config, report.Report, error) { return TranslateFromV2_1(cfg), report, err } +func ParseFromV2_2(rawConfig []byte) (types.Config, report.Report, error) { + cfg, report, err := v2_2.Parse(rawConfig) + if err != nil { + return types.Config{}, report, err + } + + return TranslateFromV2_2(cfg), report, err +} + func Version(rawConfig []byte) (semver.Version, error) { var composite struct { Version *int `json:"ignitionVersion"` diff --git a/config/config_test.go b/config/config_test.go index af00f2036..1bd22a561 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -56,7 +56,7 @@ func TestParse(t *testing.T) { }, { in: in{config: []byte(`{"ignition": {"version": "2.2.0-experimental"}}`)}, - out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + out: out{err: ErrUnknownVersion}, }, { in: in{config: []byte(`{"ignition": {"version": "2.1.0-experimental"}}`)}, @@ -64,7 +64,11 @@ func TestParse(t *testing.T) { }, { in: in{config: []byte(`{"ignition": {"version": "2.2.0"}}`)}, - out: out{err: ErrInvalid}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.3.0-experimental"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, }, { in: in{config: []byte(`{"ignition": {"version": "2.0.0"},}`)}, diff --git a/config/translate.go b/config/translate.go index 7fb91de03..660b90b2b 100644 --- a/config/translate.go +++ b/config/translate.go @@ -23,6 +23,7 @@ import ( v1 "github.com/coreos/ignition/config/v1/types" v2_0 "github.com/coreos/ignition/config/v2_0/types" v2_1 "github.com/coreos/ignition/config/v2_1/types" + v2_2 "github.com/coreos/ignition/config/v2_2/types" "github.com/vincent-petithory/dataurl" ) @@ -732,3 +733,362 @@ func TranslateFromV2_1(old v2_1.Config) types.Config { } return config } + +func TranslateFromV2_2(old v2_2.Config) types.Config { + translateConfigReference := func(old *v2_2.ConfigReference) *types.ConfigReference { + if old == nil { + return nil + } + return &types.ConfigReference{ + Source: old.Source, + Verification: types.Verification{ + Hash: old.Verification.Hash, + }, + } + } + translateConfigReferenceSlice := func(old []v2_2.ConfigReference) []types.ConfigReference { + var res []types.ConfigReference + for _, c := range old { + res = append(res, *translateConfigReference(&c)) + } + return res + } + translateCertificateAuthoritySlice := func(old []v2_2.CaReference) []types.CaReference { + var res []types.CaReference + for _, x := range old { + res = append(res, types.CaReference{ + Source: x.Source, + Verification: types.Verification{ + Hash: x.Verification.Hash, + }, + }) + } + return res + } + translateNetworkdDropinSlice := func(old []v2_2.NetworkdDropin) []types.NetworkdDropin { + var res []types.NetworkdDropin + for _, x := range old { + res = append(res, types.NetworkdDropin{ + Contents: x.Contents, + Name: x.Name, + }) + } + return res + } + translateNetworkdUnitSlice := func(old []v2_2.Networkdunit) []types.Networkdunit { + var res []types.Networkdunit + for _, u := range old { + res = append(res, types.Networkdunit{ + Contents: u.Contents, + Name: u.Name, + Dropins: translateNetworkdDropinSlice(u.Dropins), + }) + } + return res + } + translatePasswdGroupSlice := func(old []v2_2.PasswdGroup) []types.PasswdGroup { + var res []types.PasswdGroup + for _, g := range old { + res = append(res, types.PasswdGroup{ + Gid: g.Gid, + Name: g.Name, + PasswordHash: g.PasswordHash, + System: g.System, + }) + } + return res + } + translatePasswdUsercreateGroupSlice := func(old []v2_2.UsercreateGroup) []types.UsercreateGroup { + var res []types.UsercreateGroup + for _, g := range old { + res = append(res, types.UsercreateGroup(g)) + } + return res + } + translatePasswdUsercreate := func(old *v2_2.Usercreate) *types.Usercreate { + if old == nil { + return nil + } + return &types.Usercreate{ + Gecos: old.Gecos, + Groups: translatePasswdUsercreateGroupSlice(old.Groups), + HomeDir: old.HomeDir, + NoCreateHome: old.NoCreateHome, + NoLogInit: old.NoLogInit, + NoUserGroup: old.NoUserGroup, + PrimaryGroup: old.PrimaryGroup, + Shell: old.Shell, + System: old.System, + UID: old.UID, + } + } + translatePasswdUserGroupSlice := func(old []v2_2.Group) []types.Group { + var res []types.Group + for _, g := range old { + res = append(res, types.Group(g)) + } + return res + } + translatePasswdSSHAuthorizedKeySlice := func(old []v2_2.SSHAuthorizedKey) []types.SSHAuthorizedKey { + res := make([]types.SSHAuthorizedKey, len(old)) + for i, k := range old { + res[i] = types.SSHAuthorizedKey(k) + } + return res + } + translatePasswdUserSlice := func(old []v2_2.PasswdUser) []types.PasswdUser { + var res []types.PasswdUser + for _, u := range old { + res = append(res, types.PasswdUser{ + Create: translatePasswdUsercreate(u.Create), + Gecos: u.Gecos, + Groups: translatePasswdUserGroupSlice(u.Groups), + HomeDir: u.HomeDir, + Name: u.Name, + NoCreateHome: u.NoCreateHome, + NoLogInit: u.NoLogInit, + NoUserGroup: u.NoUserGroup, + PasswordHash: u.PasswordHash, + PrimaryGroup: u.PrimaryGroup, + SSHAuthorizedKeys: translatePasswdSSHAuthorizedKeySlice(u.SSHAuthorizedKeys), + Shell: u.Shell, + System: u.System, + UID: u.UID, + }) + } + return res + } + translateNodeGroup := func(old *v2_2.NodeGroup) *types.NodeGroup { + if old == nil { + return nil + } + return &types.NodeGroup{ + ID: old.ID, + Name: old.Name, + } + } + translateNodeUser := func(old *v2_2.NodeUser) *types.NodeUser { + if old == nil { + return nil + } + return &types.NodeUser{ + ID: old.ID, + Name: old.Name, + } + } + translateNode := func(old v2_2.Node) types.Node { + return types.Node{ + Filesystem: old.Filesystem, + Group: translateNodeGroup(old.Group), + Path: old.Path, + User: translateNodeUser(old.User), + Overwrite: old.Overwrite, + } + } + translateDirectorySlice := func(old []v2_2.Directory) []types.Directory { + var res []types.Directory + for _, x := range old { + res = append(res, types.Directory{ + Node: translateNode(x.Node), + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: x.DirectoryEmbedded1.Mode, + }, + }) + } + return res + } + translatePartitionSlice := func(old []v2_2.Partition) []types.Partition { + var res []types.Partition + for _, x := range old { + res = append(res, types.Partition{ + GUID: x.GUID, + Label: x.Label, + Number: x.Number, + Size: x.Size, + Start: x.Start, + TypeGUID: x.TypeGUID, + }) + } + return res + } + translateDiskSlice := func(old []v2_2.Disk) []types.Disk { + var res []types.Disk + for _, x := range old { + res = append(res, types.Disk{ + Device: x.Device, + Partitions: translatePartitionSlice(x.Partitions), + WipeTable: x.WipeTable, + }) + } + return res + } + translateFileSlice := func(old []v2_2.File) []types.File { + var res []types.File + for _, x := range old { + res = append(res, types.File{ + Node: translateNode(x.Node), + FileEmbedded1: types.FileEmbedded1{ + Contents: types.FileContents{ + Compression: x.Contents.Compression, + Source: x.Contents.Source, + Verification: types.Verification{ + Hash: x.Contents.Verification.Hash, + }, + }, + Mode: x.Mode, + Append: x.Append, + }, + }) + } + return res + } + translateMountCreateOptionSlice := func(old []v2_2.CreateOption) []types.CreateOption { + var res []types.CreateOption + for _, x := range old { + res = append(res, types.CreateOption(x)) + } + return res + } + translateMountCreate := func(old *v2_2.Create) *types.Create { + if old == nil { + return nil + } + return &types.Create{ + Force: old.Force, + Options: translateMountCreateOptionSlice(old.Options), + } + } + translateMountOptionSlice := func(old []v2_2.MountOption) []types.MountOption { + var res []types.MountOption + for _, x := range old { + res = append(res, types.MountOption(x)) + } + return res + } + translateMount := func(old *v2_2.Mount) *types.Mount { + if old == nil { + return nil + } + return &types.Mount{ + Create: translateMountCreate(old.Create), + Device: old.Device, + Format: old.Format, + Label: old.Label, + Options: translateMountOptionSlice(old.Options), + UUID: old.UUID, + WipeFilesystem: old.WipeFilesystem, + } + } + translateFilesystemSlice := func(old []v2_2.Filesystem) []types.Filesystem { + var res []types.Filesystem + for _, x := range old { + res = append(res, types.Filesystem{ + Mount: translateMount(x.Mount), + Name: x.Name, + Path: x.Path, + }) + } + return res + } + translateLinkSlice := func(old []v2_2.Link) []types.Link { + var res []types.Link + for _, x := range old { + res = append(res, types.Link{ + Node: translateNode(x.Node), + LinkEmbedded1: types.LinkEmbedded1{ + Hard: x.Hard, + Target: x.Target, + }, + }) + } + return res + } + translateDeviceSlice := func(old []v2_2.Device) []types.Device { + var res []types.Device + for _, x := range old { + res = append(res, types.Device(x)) + } + return res + } + translateRaidOptionSlice := func(old []v2_2.RaidOption) []types.RaidOption { + var res []types.RaidOption + for _, x := range old { + res = append(res, types.RaidOption(x)) + } + return res + } + translateRaidSlice := func(old []v2_2.Raid) []types.Raid { + var res []types.Raid + for _, x := range old { + res = append(res, types.Raid{ + Devices: translateDeviceSlice(x.Devices), + Level: x.Level, + Name: x.Name, + Spares: x.Spares, + Options: translateRaidOptionSlice(x.Options), + }) + } + return res + } + translateSystemdDropinSlice := func(old []v2_2.SystemdDropin) []types.SystemdDropin { + var res []types.SystemdDropin + for _, x := range old { + res = append(res, types.SystemdDropin{ + Contents: x.Contents, + Name: x.Name, + }) + } + return res + } + translateSystemdUnitSlice := func(old []v2_2.Unit) []types.Unit { + var res []types.Unit + for _, x := range old { + res = append(res, types.Unit{ + Contents: x.Contents, + Dropins: translateSystemdDropinSlice(x.Dropins), + Enable: x.Enable, + Enabled: x.Enabled, + Mask: x.Mask, + Name: x.Name, + }) + } + return res + } + config := types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Timeouts: types.Timeouts{ + HTTPResponseHeaders: old.Ignition.Timeouts.HTTPResponseHeaders, + HTTPTotal: old.Ignition.Timeouts.HTTPTotal, + }, + Config: types.IgnitionConfig{ + Replace: translateConfigReference(old.Ignition.Config.Replace), + Append: translateConfigReferenceSlice(old.Ignition.Config.Append), + }, + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: translateCertificateAuthoritySlice(old.Ignition.Security.TLS.CertificateAuthorities), + }, + }, + }, + Networkd: types.Networkd{ + Units: translateNetworkdUnitSlice(old.Networkd.Units), + }, + Passwd: types.Passwd{ + Groups: translatePasswdGroupSlice(old.Passwd.Groups), + Users: translatePasswdUserSlice(old.Passwd.Users), + }, + Storage: types.Storage{ + Directories: translateDirectorySlice(old.Storage.Directories), + Disks: translateDiskSlice(old.Storage.Disks), + Files: translateFileSlice(old.Storage.Files), + Filesystems: translateFilesystemSlice(old.Storage.Filesystems), + Links: translateLinkSlice(old.Storage.Links), + Raid: translateRaidSlice(old.Storage.Raid), + }, + Systemd: types.Systemd{ + Units: translateSystemdUnitSlice(old.Systemd.Units), + }, + } + return config +} diff --git a/config/translate_test.go b/config/translate_test.go index a93db3a3d..65db1626f 100644 --- a/config/translate_test.go +++ b/config/translate_test.go @@ -25,6 +25,7 @@ import ( v1 "github.com/coreos/ignition/config/v1/types" v2_0 "github.com/coreos/ignition/config/v2_0/types" v2_1 "github.com/coreos/ignition/config/v2_1/types" + v2_2 "github.com/coreos/ignition/config/v2_2/types" ) func TestTranslateFromV1(t *testing.T) { @@ -1780,3 +1781,981 @@ func TestTranslateFromV2_1(t *testing.T) { assert.Equal(t, test.out.config, config, "#%d: bad config", i) } } + +func TestTranslateFromV2_2(t *testing.T) { + type in struct { + config v2_2.Config + } + type out struct { + config types.Config + } + + tests := []struct { + in in + out out + }{ + { + in: in{}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Config: v2_2.IgnitionConfig{ + Append: []v2_2.ConfigReference{ + { + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file1", + }).String(), + }, + { + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file2", + }).String(), + Verification: v2_2.Verification{ + Hash: strToPtr("func2-sum2"), + }, + }, + }, + Replace: &v2_2.ConfigReference{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file3", + }).String(), + Verification: v2_2.Verification{ + Hash: strToPtr("func3-sum3"), + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Config: types.IgnitionConfig{ + Append: []types.ConfigReference{ + { + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file1", + }).String(), + }, + { + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file2", + }).String(), + Verification: types.Verification{ + Hash: strToPtr("func2-sum2"), + }, + }, + }, + Replace: &types.ConfigReference{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file3", + }).String(), + Verification: types.Verification{ + Hash: strToPtr("func3-sum3"), + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Timeouts: v2_2.Timeouts{ + HTTPResponseHeaders: nil, + HTTPTotal: nil, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Timeouts: types.Timeouts{ + HTTPResponseHeaders: nil, + HTTPTotal: nil, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Timeouts: v2_2.Timeouts{ + HTTPResponseHeaders: intToPtr(0), + HTTPTotal: intToPtr(0), + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Timeouts: types.Timeouts{ + HTTPResponseHeaders: intToPtr(0), + HTTPTotal: intToPtr(0), + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Timeouts: v2_2.Timeouts{ + HTTPResponseHeaders: intToPtr(50), + HTTPTotal: intToPtr(100), + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Timeouts: types.Timeouts{ + HTTPResponseHeaders: intToPtr(50), + HTTPTotal: intToPtr(100), + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Security: v2_2.Security{ + TLS: v2_2.TLS{ + CertificateAuthorities: []v2_2.CaReference{ + { + Source: "https://example.com/ca.pem", + }, + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.CaReference{ + { + Source: "https://example.com/ca.pem", + }, + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{ + Security: v2_2.Security{ + TLS: v2_2.TLS{ + CertificateAuthorities: []v2_2.CaReference{ + { + Source: "https://example.com/ca.pem", + }, + { + Source: "https://example.com/ca2.pem", + Verification: v2_2.Verification{ + Hash: strToPtr("sha512-adbebebd234245380"), + }, + }, + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: types.MaxVersion.String(), + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.CaReference{ + { + Source: "https://example.com/ca.pem", + }, + { + Source: "https://example.com/ca2.pem", + Verification: types.Verification{ + Hash: strToPtr("sha512-adbebebd234245380"), + }, + }, + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Disks: []v2_2.Disk{ + { + Device: "/dev/sda", + WipeTable: true, + Partitions: []v2_2.Partition{ + { + Label: "ROOT", + Number: 7, + Size: 100, + Start: 50, + TypeGUID: "HI", + GUID: "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", + }, + { + Label: "DATA", + Number: 12, + Size: 1000, + Start: 300, + TypeGUID: "LO", + GUID: "3B8F8425-20E0-4F3B-907F-1A25A76F98E8", + }, + }, + }, + { + Device: "/dev/sdb", + WipeTable: true, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/sda", + WipeTable: true, + Partitions: []types.Partition{ + { + Label: "ROOT", + Number: 7, + Size: 100, + Start: 50, + TypeGUID: "HI", + GUID: "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709", + }, + { + Label: "DATA", + Number: 12, + Size: 1000, + Start: 300, + TypeGUID: "LO", + GUID: "3B8F8425-20E0-4F3B-907F-1A25A76F98E8", + }, + }, + }, + { + Device: "/dev/sdb", + WipeTable: true, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Raid: []v2_2.Raid{ + { + Name: "fast", + Level: "raid0", + Devices: []v2_2.Device{ + v2_2.Device("/dev/sdc"), + v2_2.Device("/dev/sdd"), + }, + Spares: 2, + }, + { + Name: "durable", + Level: "raid1", + Devices: []v2_2.Device{ + v2_2.Device("/dev/sde"), + v2_2.Device("/dev/sdf"), + }, + Spares: 3, + }, + { + Name: "fast-and-durable", + Level: "raid10", + Devices: []v2_2.Device{ + v2_2.Device("/dev/sdg"), + v2_2.Device("/dev/sdh"), + v2_2.Device("/dev/sdi"), + v2_2.Device("/dev/sdj"), + }, + Spares: 0, + Options: []v2_2.RaidOption{ + "--this-is-a-flag", + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Raid: []types.Raid{ + { + Name: "fast", + Level: "raid0", + Devices: []types.Device{types.Device("/dev/sdc"), types.Device("/dev/sdd")}, + Spares: 2, + }, + { + Name: "durable", + Level: "raid1", + Devices: []types.Device{types.Device("/dev/sde"), types.Device("/dev/sdf")}, + Spares: 3, + }, + { + Name: "fast-and-durable", + Level: "raid10", + Devices: []types.Device{ + types.Device("/dev/sdg"), + types.Device("/dev/sdh"), + types.Device("/dev/sdi"), + types.Device("/dev/sdj"), + }, + Spares: 0, + Options: []types.RaidOption{ + "--this-is-a-flag", + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Filesystems: []v2_2.Filesystem{ + { + Name: "filesystem-0", + Mount: &v2_2.Mount{ + Device: "/dev/disk/by-partlabel/ROOT", + Format: "btrfs", + Create: &v2_2.Create{ + Force: true, + Options: []v2_2.CreateOption{"-L", "ROOT"}, + }, + Label: strToPtr("ROOT"), + Options: []v2_2.MountOption{"--nodiscard"}, + UUID: strToPtr("8A7A6E26-5E8F-4CCA-A654-46215D4696AC"), + WipeFilesystem: true, + }, + }, + { + Name: "filesystem-1", + Mount: &v2_2.Mount{ + Device: "/dev/disk/by-partlabel/DATA", + Format: "ext4", + Label: strToPtr("DATA"), + Options: []v2_2.MountOption{"-b", "1024"}, + UUID: strToPtr("8A7A6E26-5E8F-4CCA-A654-DEADBEEF0101"), + WipeFilesystem: false, + }, + }, + { + Name: "filesystem-2", + Path: func(p string) *string { return &p }("/foo"), + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Name: "filesystem-0", + Mount: &types.Mount{ + Device: "/dev/disk/by-partlabel/ROOT", + Format: "btrfs", + Create: &types.Create{ + Force: true, + Options: []types.CreateOption{"-L", "ROOT"}, + }, + Label: strToPtr("ROOT"), + Options: []types.MountOption{"--nodiscard"}, + UUID: strToPtr("8A7A6E26-5E8F-4CCA-A654-46215D4696AC"), + WipeFilesystem: true, + }, + }, + { + Name: "filesystem-1", + Mount: &types.Mount{ + Device: "/dev/disk/by-partlabel/DATA", + Format: "ext4", + Label: strToPtr("DATA"), + Options: []types.MountOption{"-b", "1024"}, + UUID: strToPtr("8A7A6E26-5E8F-4CCA-A654-DEADBEEF0101"), + WipeFilesystem: false, + }, + }, + { + Name: "filesystem-2", + Path: strToPtr("/foo"), + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Files: []v2_2.File{ + { + Node: v2_2.Node{ + Filesystem: "filesystem-0", + Path: "/opt/file1", + User: &v2_2.NodeUser{ID: intToPtr(500)}, + Group: &v2_2.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(false), + }, + FileEmbedded1: v2_2.FileEmbedded1{ + Mode: intToPtr(0664), + Contents: v2_2.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file1", + }).String(), + Verification: v2_2.Verification{ + Hash: strToPtr("foobar"), + }, + }, + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-0", + Path: "/opt/file2", + User: &v2_2.NodeUser{ID: intToPtr(502)}, + Group: &v2_2.NodeGroup{ID: intToPtr(503)}, + }, + FileEmbedded1: v2_2.FileEmbedded1{ + Mode: intToPtr(0644), + Append: true, + Contents: v2_2.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file2", + }).String(), + Compression: "gzip", + }, + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-1", + Path: "/opt/file3", + User: &v2_2.NodeUser{ID: intToPtr(1000)}, + Group: &v2_2.NodeGroup{ID: intToPtr(1001)}, + }, + FileEmbedded1: v2_2.FileEmbedded1{ + Mode: intToPtr(0400), + Contents: v2_2.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file3", + }).String(), + }, + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Filesystem: "filesystem-0", + Path: "/opt/file1", + User: &types.NodeUser{ID: intToPtr(500)}, + Group: &types.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(false), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: intToPtr(0664), + Contents: types.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file1", + }).String(), + Verification: types.Verification{ + Hash: strToPtr("foobar"), + }, + }, + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-0", + Path: "/opt/file2", + User: &types.NodeUser{ID: intToPtr(502)}, + Group: &types.NodeGroup{ID: intToPtr(503)}, + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: intToPtr(0644), + Append: true, + Contents: types.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file2", + }).String(), + Compression: "gzip", + }, + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-1", + Path: "/opt/file3", + User: &types.NodeUser{ID: intToPtr(1000)}, + Group: &types.NodeGroup{ID: intToPtr(1001)}, + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: intToPtr(0400), + Contents: types.FileContents{ + Source: (&url.URL{ + Scheme: "data", + Opaque: ",file3", + }).String(), + }, + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Directories: []v2_2.Directory{ + { + Node: v2_2.Node{ + Filesystem: "filesystem-1", + Path: "/opt/dir1", + User: &v2_2.NodeUser{ID: intToPtr(500)}, + Group: &v2_2.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(false), + }, + DirectoryEmbedded1: v2_2.DirectoryEmbedded1{ + Mode: intToPtr(0664), + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-2", + Path: "/opt/dir2", + User: &v2_2.NodeUser{ID: intToPtr(502)}, + Group: &v2_2.NodeGroup{ID: intToPtr(503)}, + }, + DirectoryEmbedded1: v2_2.DirectoryEmbedded1{ + Mode: intToPtr(0644), + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-2", + Path: "/opt/dir3", + User: &v2_2.NodeUser{ID: intToPtr(1000)}, + Group: &v2_2.NodeGroup{ID: intToPtr(1001)}, + }, + DirectoryEmbedded1: v2_2.DirectoryEmbedded1{ + Mode: intToPtr(0400), + }, + }, + }, + }}, + }, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Directories: []types.Directory{ + { + Node: types.Node{ + Filesystem: "filesystem-1", + Path: "/opt/dir1", + User: &types.NodeUser{ID: intToPtr(500)}, + Group: &types.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(false), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: intToPtr(0664), + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-2", + Path: "/opt/dir2", + User: &types.NodeUser{ID: intToPtr(502)}, + Group: &types.NodeGroup{ID: intToPtr(503)}, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: intToPtr(0644), + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-2", + Path: "/opt/dir3", + User: &types.NodeUser{ID: intToPtr(1000)}, + Group: &types.NodeGroup{ID: intToPtr(1001)}, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: intToPtr(0400), + }, + }, + }, + }, + }}, + }, + { + in: in{config: v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Storage: v2_2.Storage{ + Links: []v2_2.Link{ + { + Node: v2_2.Node{ + Filesystem: "filesystem-1", + Path: "/opt/link1", + User: &v2_2.NodeUser{ID: intToPtr(500)}, + Group: &v2_2.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(true), + }, + LinkEmbedded1: v2_2.LinkEmbedded1{ + Hard: false, + Target: "/opt/file1", + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-2", + Path: "/opt/link2", + User: &v2_2.NodeUser{ID: intToPtr(502)}, + Group: &v2_2.NodeGroup{ID: intToPtr(503)}, + }, + LinkEmbedded1: v2_2.LinkEmbedded1{ + Hard: true, + Target: "/opt/file2", + }, + }, + { + Node: v2_2.Node{ + Filesystem: "filesystem-2", + Path: "/opt/link3", + User: &v2_2.NodeUser{ID: intToPtr(1000)}, + Group: &v2_2.NodeGroup{ID: intToPtr(1001)}, + }, + LinkEmbedded1: v2_2.LinkEmbedded1{ + Hard: true, + Target: "/opt/file3", + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Storage: types.Storage{ + Links: []types.Link{ + { + Node: types.Node{ + Filesystem: "filesystem-1", + Path: "/opt/link1", + User: &types.NodeUser{ID: intToPtr(500)}, + Group: &types.NodeGroup{ID: intToPtr(501)}, + Overwrite: boolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Hard: false, + Target: "/opt/file1", + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-2", + Path: "/opt/link2", + User: &types.NodeUser{ID: intToPtr(502)}, + Group: &types.NodeGroup{ID: intToPtr(503)}, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Hard: true, + Target: "/opt/file2", + }, + }, + { + Node: types.Node{ + Filesystem: "filesystem-2", + Path: "/opt/link3", + User: &types.NodeUser{ID: intToPtr(1000)}, + Group: &types.NodeGroup{ID: intToPtr(1001)}, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Hard: true, + Target: "/opt/file3", + }, + }, + }, + }, + }}, + }, + { + in: in{v2_2.Config{ + Systemd: v2_2.Systemd{ + Units: []v2_2.Unit{ + { + Name: "test1.service", + Enable: true, + Contents: "test1 contents", + Dropins: []v2_2.SystemdDropin{ + { + Name: "conf1.conf", + Contents: "conf1 contents", + }, + { + Name: "conf2.conf", + Contents: "conf2 contents", + }, + }, + }, + { + Name: "test2.service", + Mask: true, + Contents: "test2 contents", + }, + { + Name: "test3.service", + Enabled: boolToPtr(false), + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Name: "test1.service", + Enable: true, + Contents: "test1 contents", + Dropins: []types.SystemdDropin{ + { + Name: "conf1.conf", + Contents: "conf1 contents", + }, + { + Name: "conf2.conf", + Contents: "conf2 contents", + }, + }, + }, + { + Name: "test2.service", + Mask: true, + Contents: "test2 contents", + }, + { + Name: "test3.service", + Enabled: boolToPtr(false), + }, + }, + }, + }}, + }, + { + in: in{v2_2.Config{ + Networkd: v2_2.Networkd{ + Units: []v2_2.Networkdunit{ + { + Name: "test1.network", + Contents: "test1 contents", + }, + { + Name: "test2.network", + Dropins: []v2_2.NetworkdDropin{ + { + Name: "conf1.conf", + Contents: "conf1 contents", + }, + { + Name: "conf2.conf", + Contents: "conf2 contents", + }, + }, + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Networkd: types.Networkd{ + Units: []types.Networkdunit{ + { + Name: "test1.network", + Contents: "test1 contents", + }, + { + Name: "test2.network", + Dropins: []types.NetworkdDropin{ + { + Name: "conf1.conf", + Contents: "conf1 contents", + }, + { + Name: "conf2.conf", + Contents: "conf2 contents", + }, + }, + }, + }, + }, + }}, + }, + { + in: in{v2_2.Config{ + Ignition: v2_2.Ignition{Version: v2_2.MaxVersion.String()}, + Passwd: v2_2.Passwd{ + Users: []v2_2.PasswdUser{ + { + Name: "user 1", + PasswordHash: strToPtr("password 1"), + SSHAuthorizedKeys: []v2_2.SSHAuthorizedKey{"key1", "key2"}, + }, + { + Name: "user 2", + PasswordHash: strToPtr("password 2"), + SSHAuthorizedKeys: []v2_2.SSHAuthorizedKey{"key3", "key4"}, + Create: &v2_2.Usercreate{ + UID: intToPtr(123), + Gecos: "gecos", + HomeDir: "/home/user 2", + NoCreateHome: true, + PrimaryGroup: "wheel", + Groups: []v2_2.UsercreateGroup{"wheel", "plugdev"}, + NoUserGroup: true, + System: true, + NoLogInit: true, + Shell: "/bin/zsh", + }, + }, + { + Name: "user 3", + PasswordHash: strToPtr("password 3"), + SSHAuthorizedKeys: []v2_2.SSHAuthorizedKey{"key5", "key6"}, + UID: intToPtr(123), + Gecos: "gecos", + HomeDir: "/home/user 2", + NoCreateHome: true, + PrimaryGroup: "wheel", + Groups: []v2_2.Group{"wheel", "plugdev"}, + NoUserGroup: true, + System: true, + NoLogInit: true, + Shell: "/bin/zsh", + }, + { + Name: "user 4", + PasswordHash: strToPtr("password 4"), + SSHAuthorizedKeys: []v2_2.SSHAuthorizedKey{"key7", "key8"}, + Create: &v2_2.Usercreate{}, + }, + }, + Groups: []v2_2.PasswdGroup{ + { + Name: "group 1", + Gid: func(i int) *int { return &i }(1000), + PasswordHash: "password 1", + System: true, + }, + { + Name: "group 2", + PasswordHash: "password 2", + }, + }, + }, + }}, + out: out{config: types.Config{ + Ignition: types.Ignition{Version: types.MaxVersion.String()}, + Passwd: types.Passwd{ + Users: []types.PasswdUser{ + { + Name: "user 1", + PasswordHash: strToPtr("password 1"), + SSHAuthorizedKeys: []types.SSHAuthorizedKey{"key1", "key2"}, + }, + { + Name: "user 2", + PasswordHash: strToPtr("password 2"), + SSHAuthorizedKeys: []types.SSHAuthorizedKey{"key3", "key4"}, + Create: &types.Usercreate{ + UID: func(i int) *int { return &i }(123), + Gecos: "gecos", + HomeDir: "/home/user 2", + NoCreateHome: true, + PrimaryGroup: "wheel", + Groups: []types.UsercreateGroup{"wheel", "plugdev"}, + NoUserGroup: true, + System: true, + NoLogInit: true, + Shell: "/bin/zsh", + }, + }, + { + Name: "user 3", + PasswordHash: strToPtr("password 3"), + SSHAuthorizedKeys: []types.SSHAuthorizedKey{"key5", "key6"}, + UID: intToPtr(123), + Gecos: "gecos", + HomeDir: "/home/user 2", + NoCreateHome: true, + PrimaryGroup: "wheel", + Groups: []types.Group{"wheel", "plugdev"}, + NoUserGroup: true, + System: true, + NoLogInit: true, + Shell: "/bin/zsh", + }, + { + Name: "user 4", + PasswordHash: strToPtr("password 4"), + SSHAuthorizedKeys: []types.SSHAuthorizedKey{"key7", "key8"}, + Create: &types.Usercreate{}, + }, + }, + Groups: []types.PasswdGroup{ + { + Name: "group 1", + Gid: func(i int) *int { return &i }(1000), + PasswordHash: "password 1", + System: true, + }, + { + Name: "group 2", + PasswordHash: "password 2", + }, + }, + }, + }}, + }, + } + + for i, test := range tests { + config := TranslateFromV2_2(test.in.config) + assert.Equal(t, test.out.config, config, "#%d: bad config", i) + } +} diff --git a/config/types/config.go b/config/types/config.go index b948a1643..cfef0ed9d 100644 --- a/config/types/config.go +++ b/config/types/config.go @@ -25,7 +25,7 @@ import ( var ( MaxVersion = semver.Version{ Major: 2, - Minor: 2, + Minor: 3, PreRelease: "experimental", } ) diff --git a/config/v2_2/append.go b/config/v2_2/append.go new file mode 100644 index 000000000..cf28f4090 --- /dev/null +++ b/config/v2_2/append.go @@ -0,0 +1,76 @@ +// Copyright 2016 CoreOS, 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 v2_2 + +import ( + "reflect" + + "github.com/coreos/ignition/config/v2_2/types" +) + +// Append appends newConfig to oldConfig and returns the result. Appending one +// config to another is accomplished by iterating over every field in the +// config structure, appending slices, recursively appending structs, and +// overwriting old values with new values for all other types. +func Append(oldConfig, newConfig types.Config) types.Config { + vOld := reflect.ValueOf(oldConfig) + vNew := reflect.ValueOf(newConfig) + + vResult := appendStruct(vOld, vNew) + + return vResult.Interface().(types.Config) +} + +// appendStruct is an internal helper function to AppendConfig. Given two values +// of structures (assumed to be the same type), recursively iterate over every +// field in the struct, appending slices, recursively appending structs, and +// overwriting old values with the new for all other types. Some individual +// struct fields have alternate merge strategies, determined by the field name. +// Currently these fields are "ignition.version", which uses the old value, and +// "ignition.config" which uses the new value. +func appendStruct(vOld, vNew reflect.Value) reflect.Value { + tOld := vOld.Type() + vRes := reflect.New(tOld) + + for i := 0; i < tOld.NumField(); i++ { + vfOld := vOld.Field(i) + vfNew := vNew.Field(i) + vfRes := vRes.Elem().Field(i) + + switch tOld.Field(i).Name { + case "Version": + vfRes.Set(vfOld) + continue + case "Config": + vfRes.Set(vfNew) + continue + } + + switch vfOld.Type().Kind() { + case reflect.Struct: + vfRes.Set(appendStruct(vfOld, vfNew)) + case reflect.Slice: + vfRes.Set(reflect.AppendSlice(vfOld, vfNew)) + default: + if vfNew.Kind() == reflect.Ptr && vfNew.IsNil() { + vfRes.Set(vfOld) + } else { + vfRes.Set(vfNew) + } + } + } + + return vRes.Elem() +} diff --git a/config/v2_2/append_test.go b/config/v2_2/append_test.go new file mode 100644 index 000000000..571fc1a9e --- /dev/null +++ b/config/v2_2/append_test.go @@ -0,0 +1,228 @@ +// Copyright 2016 CoreOS, 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 v2_2 + +import ( + "testing" + + "github.com/coreos/go-semver/semver" + "github.com/stretchr/testify/assert" + + "github.com/coreos/ignition/config/v2_2/types" +) + +func intToPtr(x int) *int { + return &x +} + +func TestAppend(t *testing.T) { + type in struct { + oldConfig types.Config + newConfig types.Config + } + type out struct { + config types.Config + } + + tests := []struct { + in in + out out + }{ + // empty + { + in: in{ + oldConfig: types.Config{}, + newConfig: types.Config{}, + }, + out: out{config: types.Config{}}, + }, + + // merge tags + { + in: in{ + oldConfig: types.Config{}, + newConfig: types.Config{ + Ignition: types.Ignition{ + Version: semver.Version{Major: 2}.String(), + }, + }, + }, + out: out{config: types.Config{}}, + }, + { + in: in{ + oldConfig: types.Config{ + Ignition: types.Ignition{ + Version: semver.Version{Major: 2}.String(), + }, + }, + newConfig: types.Config{}, + }, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Version: semver.Version{Major: 2}.String(), + }, + }}, + }, + { + in: in{ + oldConfig: types.Config{}, + newConfig: types.Config{ + Ignition: types.Ignition{ + Config: types.IgnitionConfig{ + Replace: &types.ConfigReference{}, + }, + }, + }, + }, + out: out{config: types.Config{ + Ignition: types.Ignition{ + Config: types.IgnitionConfig{ + Replace: &types.ConfigReference{}, + }, + }, + }}, + }, + { + in: in{ + oldConfig: types.Config{ + Ignition: types.Ignition{ + Config: types.IgnitionConfig{ + Replace: &types.ConfigReference{}, + }, + }, + }, + newConfig: types.Config{}, + }, + out: out{config: types.Config{}}, + }, + + // old + { + in: in{ + oldConfig: types.Config{ + Storage: types.Storage{ + Disks: []types.Disk{ + { + WipeTable: true, + Partitions: []types.Partition{ + {Number: 1}, + {Number: 2}, + }, + }, + }, + }, + }, + newConfig: types.Config{}, + }, + out: out{config: types.Config{ + Storage: types.Storage{ + Disks: []types.Disk{ + { + WipeTable: true, + Partitions: []types.Partition{ + {Number: 1}, + {Number: 2}, + }, + }, + }, + }, + }}, + }, + + // new + { + in: in{ + oldConfig: types.Config{}, + newConfig: types.Config{ + Systemd: types.Systemd{ + Units: []types.Unit{ + {Name: "test1.service"}, + {Name: "test2.service"}, + }, + }, + }, + }, + out: out{config: types.Config{ + Systemd: types.Systemd{ + Units: []types.Unit{ + {Name: "test1.service"}, + {Name: "test2.service"}, + }, + }, + }}, + }, + + // both + { + in: in{ + oldConfig: types.Config{ + Passwd: types.Passwd{ + Users: []types.PasswdUser{ + {Name: "oldUser"}, + }, + }, + }, + newConfig: types.Config{ + Passwd: types.Passwd{ + Users: []types.PasswdUser{ + {Name: "newUser"}, + }, + }, + }, + }, + out: out{config: types.Config{ + Passwd: types.Passwd{ + Users: []types.PasswdUser{ + {Name: "oldUser"}, + {Name: "newUser"}, + }, + }, + }}, + }, + + // when the newconfig doesn't set a value + { + in: in{ + oldConfig: types.Config{ + Ignition: types.Ignition{ + Timeouts: types.Timeouts{ + HTTPTotal: intToPtr(1), + }, + }, + }, + newConfig: types.Config{ + Ignition: types.Ignition{ + Timeouts: types.Timeouts{ + HTTPTotal: nil, + }, + }, + }, + }, + out: out{types.Config{ + Ignition: types.Ignition{ + Timeouts: types.Timeouts{ + HTTPTotal: intToPtr(1), + }, + }, + }}, + }, + } + + for i, test := range tests { + config := Append(test.in.oldConfig, test.in.newConfig) + assert.Equal(t, test.out.config, config, "#%d: bad config", i) + } +} diff --git a/config/v2_2/cloudinit.go b/config/v2_2/cloudinit.go new file mode 100644 index 000000000..36a543932 --- /dev/null +++ b/config/v2_2/cloudinit.go @@ -0,0 +1,53 @@ +// Copyright 2015 CoreOS, 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. + +// These functions are copied from github.com/coreos/coreos-cloudinit/config. + +package v2_2 + +import ( + "bytes" + "compress/gzip" + "io/ioutil" + "strings" + "unicode" +) + +func isCloudConfig(userdata []byte) bool { + header := strings.SplitN(string(decompressIfGzipped(userdata)), "\n", 2)[0] + + // Trim trailing whitespaces + header = strings.TrimRightFunc(header, unicode.IsSpace) + + return (header == "#cloud-config") +} + +func isScript(userdata []byte) bool { + header := strings.SplitN(string(decompressIfGzipped(userdata)), "\n", 2)[0] + return strings.HasPrefix(header, "#!") +} + +func decompressIfGzipped(data []byte) []byte { + if reader, err := gzip.NewReader(bytes.NewReader(data)); err == nil { + uncompressedData, err := ioutil.ReadAll(reader) + reader.Close() + if err == nil { + return uncompressedData + } else { + return data + } + } else { + return data + } +} diff --git a/config/v2_2/config.go b/config/v2_2/config.go new file mode 100644 index 000000000..b78d979f4 --- /dev/null +++ b/config/v2_2/config.go @@ -0,0 +1,155 @@ +// Copyright 2015 CoreOS, 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 v2_2 + +import ( + "bytes" + "errors" + "reflect" + + "github.com/coreos/ignition/config/v2_2/types" + "github.com/coreos/ignition/config/validate" + astjson "github.com/coreos/ignition/config/validate/astjson" + "github.com/coreos/ignition/config/validate/report" + + json "github.com/ajeddeloh/go-json" + "github.com/coreos/go-semver/semver" + "go4.org/errorutil" +) + +var ( + ErrCloudConfig = errors.New("not a config (found coreos-cloudconfig)") + ErrEmpty = errors.New("not a config (empty)") + ErrScript = errors.New("not a config (found coreos-cloudinit script)") + ErrDeprecated = errors.New("config format deprecated") + ErrInvalid = errors.New("config is not valid") + ErrUnknownVersion = errors.New("unsupported config version") + ErrVersionIndeterminable = errors.New("unable to determine version") + ErrBadVersion = errors.New("config must be of version 2.2.0") +) + +// Parse parses the raw config into a types.Config struct and generates a report of any +// errors, warnings, info, and deprecations it encountered. Unlike config.Parse, +// it does not attempt to translate the config. +func Parse(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 + } + + version, err := Version(rawConfig) + if err != nil { + return types.Config{}, report.ReportFromError(err, report.EntryError), err + } + if (version != semver.Version{Major: 2, Minor: 2}) { + return types.Config{}, report.ReportFromError(ErrBadVersion, report.EntryError), ErrBadVersion + } + + 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.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), true)) + } + + if r.IsFatal() { + return types.Config{}, r, ErrInvalid + } + + return config, r, nil +} + +func Version(rawConfig []byte) (semver.Version, error) { + var composite struct { + Version *int `json:"ignitionVersion"` + Ignition struct { + Version *string `json:"version"` + } `json:"ignition"` + } + + if json.Unmarshal(rawConfig, &composite) == nil { + if composite.Ignition.Version != nil { + v, err := types.Ignition{Version: *composite.Ignition.Version}.Semver() + if err != nil { + return semver.Version{}, err + } + return *v, nil + } else if composite.Version != nil { + return semver.Version{Major: int64(*composite.Version)}, nil + } + } + + return semver.Version{}, ErrVersionIndeterminable +} + +func isEmpty(userdata []byte) bool { + return len(userdata) == 0 +} diff --git a/config/v2_2/config_test.go b/config/v2_2/config_test.go new file mode 100644 index 000000000..57c2c2077 --- /dev/null +++ b/config/v2_2/config_test.go @@ -0,0 +1,122 @@ +// Copyright 2015 CoreOS, 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 v2_2 + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/config/v2_2/types" + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + type in struct { + config []byte + } + type out struct { + config types.Config + err error + checkOnStrings bool + } + + tests := []struct { + in in + out out + }{ + { + in: in{config: []byte(`{"ignitionVersion": 1}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "1.0.0"}}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.0.0"}}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.1.0"}}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.2.0-experimental"}}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.1.0-experimental"}}`)}, + out: out{err: ErrBadVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.2.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.0.0"},}`)}, + out: out{err: ErrVersionIndeterminable}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "invalid.semver"}}`)}, + out: out{err: fmt.Errorf("invalid.semver is not in dotted-tri format"), checkOnStrings: true}, + }, + { + in: in{config: []byte(`{}`)}, + out: out{err: ErrVersionIndeterminable}, + }, + { + in: in{config: []byte{}}, + out: out{err: ErrEmpty}, + }, + { + in: in{config: []byte("#cloud-config")}, + out: out{err: ErrCloudConfig}, + }, + { + in: in{config: []byte("#cloud-config ")}, + out: out{err: ErrCloudConfig}, + }, + { + in: in{config: []byte("#cloud-config\n\r")}, + out: out{err: ErrCloudConfig}, + }, + { + in: in{config: []byte{0x1f, 0x8b, 0x08, 0x00, 0x03, 0xd6, 0x79, 0x56, + 0x00, 0x03, 0x53, 0x4e, 0xce, 0xc9, 0x2f, 0x4d, 0xd1, 0x4d, 0xce, + 0xcf, 0x4b, 0xcb, 0x4c, 0xe7, 0x02, 0x00, 0x05, 0x56, 0xb3, 0xb8, + 0x0e, 0x00, 0x00, 0x00}}, + out: out{err: ErrCloudConfig}, + }, + { + in: in{config: []byte("#!/bin/sh")}, + out: out{err: ErrScript}, + }, + { + in: in{config: []byte{0x1f, 0x8b, 0x08, 0x00, 0x48, 0xda, 0x79, 0x56, + 0x00, 0x03, 0x53, 0x56, 0xd4, 0x4f, 0xca, 0xcc, 0xd3, 0x2f, 0xce, + 0xe0, 0x02, 0x00, 0x1d, 0x9d, 0xfb, 0x04, 0x0a, 0x00, 0x00, 0x00}}, + out: out{err: ErrScript}, + }, + } + + for i, test := range tests { + config, report, err := Parse(test.in.config) + if (!test.out.checkOnStrings && test.out.err != err) || + (test.out.checkOnStrings && test.out.err.Error() != err.Error()) { + t.Errorf("#%d: bad error: want %v, got %v, report: %+v", i, test.out.err, err, report) + } + assert.Equal(t, test.out.config, config, "#%d: bad config, report: %+v", i, report) + } +} diff --git a/config/v2_2/types/ca.go b/config/v2_2/types/ca.go new file mode 100644 index 000000000..93d60bb5f --- /dev/null +++ b/config/v2_2/types/ca.go @@ -0,0 +1,31 @@ +// Copyright 2018 CoreOS, 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 types + +import ( + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +func (c CaReference) ValidateSource() report.Report { + err := validateURL(c.Source) + if err != nil { + return report.ReportFromError( + fmt.Errorf("invalid url %q: %v", c.Source, err), + report.EntryError) + } + return report.Report{} +} diff --git a/config/v2_2/types/config.go b/config/v2_2/types/config.go new file mode 100644 index 000000000..b1fcfcd99 --- /dev/null +++ b/config/v2_2/types/config.go @@ -0,0 +1,91 @@ +// Copyright 2015 CoreOS, 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 types + +import ( + "fmt" + + "github.com/coreos/go-semver/semver" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + MaxVersion = semver.Version{ + Major: 2, + Minor: 2, + } +) + +func (c Config) Validate() report.Report { + r := report.Report{} + rules := []rule{ + checkFilesFilesystems, + checkDuplicateFilesystems, + } + + for _, rule := range rules { + rule(c, &r) + } + return r +} + +type rule func(cfg Config, report *report.Report) + +func checkNodeFilesystems(node Node, filesystems map[string]struct{}, nodeType string) report.Report { + r := report.Report{} + if node.Filesystem == "" { + // Filesystem was not specified. This is an error, but its handled in types.File's Validate, not here + return r + } + _, ok := filesystems[node.Filesystem] + if !ok { + r.Add(report.Entry{ + Kind: report.EntryWarning, + Message: fmt.Sprintf("%v %q references nonexistent filesystem %q. (This is ok if it is defined in a referenced config)", + nodeType, node.Path, node.Filesystem), + }) + } + return r +} + +func checkFilesFilesystems(cfg Config, r *report.Report) { + filesystems := map[string]struct{}{"root": {}} + for _, filesystem := range cfg.Storage.Filesystems { + filesystems[filesystem.Name] = struct{}{} + } + for _, file := range cfg.Storage.Files { + r.Merge(checkNodeFilesystems(file.Node, filesystems, "File")) + } + for _, link := range cfg.Storage.Links { + r.Merge(checkNodeFilesystems(link.Node, filesystems, "Link")) + } + for _, dir := range cfg.Storage.Directories { + r.Merge(checkNodeFilesystems(dir.Node, filesystems, "Directory")) + } +} + +func checkDuplicateFilesystems(cfg Config, r *report.Report) { + filesystems := map[string]struct{}{"root": {}} + for _, filesystem := range cfg.Storage.Filesystems { + if _, ok := filesystems[filesystem.Name]; ok { + r.Add(report.Entry{ + Kind: report.EntryWarning, + Message: fmt.Sprintf("Filesystem %q shadows exising filesystem definition", filesystem.Name), + }) + } + filesystems[filesystem.Name] = struct{}{} + } +} diff --git a/config/v2_2/types/directory.go b/config/v2_2/types/directory.go new file mode 100644 index 000000000..d003a7096 --- /dev/null +++ b/config/v2_2/types/directory.go @@ -0,0 +1,36 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "github.com/coreos/ignition/config/validate/report" +) + +func (d Directory) ValidateMode() report.Report { + r := report.Report{} + if err := validateMode(d.Mode); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + if d.Mode == nil { + r.Add(report.Entry{ + Message: "directory permissions unset, defaulting to 0000", + Kind: report.EntryWarning, + }) + } + return r +} diff --git a/config/v2_2/types/disk.go b/config/v2_2/types/disk.go new file mode 100644 index 000000000..f556e1826 --- /dev/null +++ b/config/v2_2/types/disk.go @@ -0,0 +1,129 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +func (n Disk) Validate() report.Report { + return report.Report{} +} + +func (n Disk) ValidateDevice() report.Report { + if len(n.Device) == 0 { + return report.ReportFromError(fmt.Errorf("disk device is required"), report.EntryError) + } + if err := validatePath(string(n.Device)); err != nil { + return report.ReportFromError(err, report.EntryError) + } + return report.Report{} +} + +func (n Disk) ValidatePartitions() report.Report { + r := report.Report{} + if n.partitionNumbersCollide() { + r.Add(report.Entry{ + Message: fmt.Sprintf("disk %q: partition numbers collide", n.Device), + Kind: report.EntryError, + }) + } + if n.partitionsOverlap() { + r.Add(report.Entry{ + Message: fmt.Sprintf("disk %q: partitions overlap", n.Device), + Kind: report.EntryError, + }) + } + if n.partitionsMisaligned() { + r.Add(report.Entry{ + Message: fmt.Sprintf("disk %q: partitions misaligned", n.Device), + Kind: report.EntryError, + }) + } + // Disks which have no errors at this point will likely succeed in sgdisk + return r +} + +// partitionNumbersCollide returns true if partition numbers in n.Partitions are not unique. +func (n Disk) partitionNumbersCollide() bool { + m := map[int][]Partition{} + for _, p := range n.Partitions { + if p.Number != 0 { + // a number of 0 means next available number, multiple devices can specify this + m[p.Number] = append(m[p.Number], p) + } + } + for _, n := range m { + if len(n) > 1 { + // TODO(vc): return information describing the collision for logging + return true + } + } + return false +} + +// end returns the last sector of a partition. +func (p Partition) end() int { + if p.Size == 0 { + // a size of 0 means "fill available", just return the start as the end for those. + return p.Start + } + return p.Start + p.Size - 1 +} + +// partitionsOverlap returns true if any explicitly dimensioned partitions overlap +func (n Disk) partitionsOverlap() bool { + for _, p := range n.Partitions { + // Starts of 0 are placed by sgdisk into the "largest available block" at that time. + // We aren't going to check those for overlap since we don't have the disk geometry. + if p.Start == 0 { + continue + } + + for _, o := range n.Partitions { + if p == o || o.Start == 0 { + continue + } + + // is p.Start within o? + if p.Start >= o.Start && p.Start <= o.end() { + return true + } + + // is p.end() within o? + if p.end() >= o.Start && p.end() <= o.end() { + return true + } + + // do p.Start and p.end() straddle o? + if p.Start < o.Start && p.end() > o.end() { + return true + } + } + } + return false +} + +// partitionsMisaligned returns true if any of the partitions don't start on a 2048-sector (1MiB) boundary. +func (n Disk) partitionsMisaligned() bool { + for _, p := range n.Partitions { + if (p.Start & (2048 - 1)) != 0 { + return true + } + } + return false +} diff --git a/config/v2_2/types/file.go b/config/v2_2/types/file.go new file mode 100644 index 000000000..57f33121e --- /dev/null +++ b/config/v2_2/types/file.go @@ -0,0 +1,76 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrAppendAndOverwrite = errors.New("cannot set both append and overwrite to true") + ErrCompressionInvalid = errors.New("invalid compression method") +) + +func (f File) Validate() report.Report { + if f.Overwrite != nil && *f.Overwrite && f.Append { + return report.ReportFromError(ErrAppendAndOverwrite, report.EntryError) + } + return report.Report{} +} + +func (f File) ValidateMode() report.Report { + r := report.Report{} + if err := validateMode(f.Mode); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + if f.Mode == nil { + r.Add(report.Entry{ + Message: "file permissions unset, defaulting to 0000", + Kind: report.EntryWarning, + }) + } + return r +} + +func (fc FileContents) ValidateCompression() report.Report { + r := report.Report{} + switch fc.Compression { + case "", "gzip": + default: + r.Add(report.Entry{ + Message: ErrCompressionInvalid.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (fc FileContents) ValidateSource() report.Report { + r := report.Report{} + err := validateURL(fc.Source) + if err != nil { + r.Add(report.Entry{ + Message: fmt.Sprintf("invalid url %q: %v", fc.Source, err), + Kind: report.EntryError, + }) + } + return r +} diff --git a/config/v2_2/types/filesystem.go b/config/v2_2/types/filesystem.go new file mode 100644 index 000000000..3c5a47d55 --- /dev/null +++ b/config/v2_2/types/filesystem.go @@ -0,0 +1,160 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrFilesystemInvalidFormat = errors.New("invalid filesystem format") + ErrFilesystemNoMountPath = errors.New("filesystem is missing mount or path") + ErrFilesystemMountAndPath = errors.New("filesystem has both mount and path defined") + ErrUsedCreateAndMountOpts = errors.New("cannot use both create object and mount-level options field") + ErrUsedCreateAndWipeFilesystem = errors.New("cannot use both create object and wipeFilesystem field") + ErrWarningCreateDeprecated = errors.New("the create object has been deprecated in favor of mount-level options") + ErrExt4LabelTooLong = errors.New("filesystem labels cannot be longer than 16 characters when using ext4") + ErrBtrfsLabelTooLong = errors.New("filesystem labels cannot be longer than 256 characters when using btrfs") + ErrXfsLabelTooLong = errors.New("filesystem labels cannot be longer than 12 characters when using xfs") + ErrSwapLabelTooLong = errors.New("filesystem labels cannot be longer than 15 characters when using swap") + ErrVfatLabelTooLong = errors.New("filesystem labels cannot be longer than 11 characters when using vfat") +) + +func (f Filesystem) Validate() report.Report { + r := report.Report{} + if f.Mount == nil && f.Path == nil { + r.Add(report.Entry{ + Message: ErrFilesystemNoMountPath.Error(), + Kind: report.EntryError, + }) + } + if f.Mount != nil { + if f.Path != nil { + r.Add(report.Entry{ + Message: ErrFilesystemMountAndPath.Error(), + Kind: report.EntryError, + }) + } + if f.Mount.Create != nil { + if f.Mount.WipeFilesystem { + r.Add(report.Entry{ + Message: ErrUsedCreateAndWipeFilesystem.Error(), + Kind: report.EntryError, + }) + } + if len(f.Mount.Options) > 0 { + r.Add(report.Entry{ + Message: ErrUsedCreateAndMountOpts.Error(), + Kind: report.EntryError, + }) + } + r.Add(report.Entry{ + Message: ErrWarningCreateDeprecated.Error(), + Kind: report.EntryWarning, + }) + } + } + return r +} + +func (f Filesystem) ValidatePath() report.Report { + r := report.Report{} + if f.Path != nil && validatePath(*f.Path) != nil { + r.Add(report.Entry{ + Message: fmt.Sprintf("filesystem %q: path not absolute", f.Name), + Kind: report.EntryError, + }) + } + return r +} + +func (m Mount) Validate() report.Report { + r := report.Report{} + switch m.Format { + case "ext4", "btrfs", "xfs", "swap", "vfat": + default: + r.Add(report.Entry{ + Message: ErrFilesystemInvalidFormat.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (m Mount) ValidateDevice() report.Report { + r := report.Report{} + if err := validatePath(m.Device); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (m Mount) ValidateLabel() report.Report { + r := report.Report{} + if m.Label == nil { + return r + } + switch m.Format { + case "ext4": + if len(*m.Label) > 16 { + // source: man mkfs.ext4 + r.Add(report.Entry{ + Message: ErrExt4LabelTooLong.Error(), + Kind: report.EntryError, + }) + } + case "btrfs": + if len(*m.Label) > 256 { + // source: man mkfs.btrfs + r.Add(report.Entry{ + Message: ErrBtrfsLabelTooLong.Error(), + Kind: report.EntryError, + }) + } + case "xfs": + if len(*m.Label) > 12 { + // source: man mkfs.xfs + r.Add(report.Entry{ + Message: ErrXfsLabelTooLong.Error(), + Kind: report.EntryError, + }) + } + case "swap": + // mkswap's man page does not state a limit on label size, but through + // experimentation it appears that mkswap will truncate long labels to + // 15 characters, so let's enforce that. + if len(*m.Label) > 15 { + r.Add(report.Entry{ + Message: ErrSwapLabelTooLong.Error(), + Kind: report.EntryError, + }) + } + case "vfat": + if len(*m.Label) > 11 { + // source: man mkfs.fat + r.Add(report.Entry{ + Message: ErrVfatLabelTooLong.Error(), + Kind: report.EntryError, + }) + } + } + return r +} diff --git a/config/v2_2/types/filesystem_test.go b/config/v2_2/types/filesystem_test.go new file mode 100644 index 000000000..c90550e48 --- /dev/null +++ b/config/v2_2/types/filesystem_test.go @@ -0,0 +1,178 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/config/validate/report" +) + +func TestMountValidate(t *testing.T) { + type in struct { + format string + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{format: "ext4"}, + out: out{}, + }, + { + in: in{format: "btrfs"}, + out: out{}, + }, + { + in: in{format: ""}, + out: out{err: ErrFilesystemInvalidFormat}, + }, + } + + for i, test := range tests { + err := Mount{Format: test.in.format, 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 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: &Mount{Device: "/foo", Format: "ext4"}}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Path: func(p string) *string { return &p }("/mount")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Path: func(p string) *string { return &p }("/mount"), Mount: &Mount{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 TestLabelValidate(t *testing.T) { + type in struct { + mount Mount + } + type out struct { + err error + } + + strToPtr := func(p string) *string { return &p } + + tests := []struct { + in in + out out + }{ + { + in: in{mount: Mount{Format: "ext4", Label: nil}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "ext4", Label: strToPtr("data")}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "ext4", Label: strToPtr("thislabelistoolong")}}, + out: out{err: ErrExt4LabelTooLong}, + }, + { + in: in{mount: Mount{Format: "btrfs", Label: nil}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "btrfs", Label: strToPtr("thislabelisnottoolong")}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "btrfs", Label: strToPtr("thislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolong")}}, + out: out{err: ErrBtrfsLabelTooLong}, + }, + { + in: in{mount: Mount{Format: "xfs", Label: nil}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "xfs", Label: strToPtr("data")}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "xfs", Label: strToPtr("thislabelistoolong")}}, + out: out{err: ErrXfsLabelTooLong}, + }, + { + in: in{mount: Mount{Format: "swap", Label: nil}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "swap", Label: strToPtr("data")}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "swap", Label: strToPtr("thislabelistoolong")}}, + out: out{err: ErrSwapLabelTooLong}, + }, + { + in: in{mount: Mount{Format: "vfat", Label: nil}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "vfat", Label: strToPtr("data")}}, + out: out{}, + }, + { + in: in{mount: Mount{Format: "vfat", Label: strToPtr("thislabelistoolong")}}, + out: out{err: ErrVfatLabelTooLong}, + }, + } + + for i, test := range tests { + err := test.in.mount.ValidateLabel() + 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) + } + } +} diff --git a/config/v2_2/types/ignition.go b/config/v2_2/types/ignition.go new file mode 100644 index 000000000..661b898d4 --- /dev/null +++ b/config/v2_2/types/ignition.go @@ -0,0 +1,59 @@ +// Copyright 2015 CoreOS, 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 types + +import ( + "errors" + + "github.com/coreos/go-semver/semver" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrOldVersion = errors.New("incorrect config version (too old)") + ErrNewVersion = errors.New("incorrect config version (too new)") + ErrInvalidVersion = errors.New("invalid config version (couldn't parse)") +) + +func (c ConfigReference) ValidateSource() report.Report { + r := report.Report{} + err := validateURL(c.Source) + if err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (v Ignition) Semver() (*semver.Version, error) { + return semver.NewVersion(v.Version) +} + +func (v Ignition) Validate() report.Report { + tv, err := v.Semver() + if err != nil { + return report.ReportFromError(ErrInvalidVersion, report.EntryError) + } + if MaxVersion.Major > tv.Major { + return report.ReportFromError(ErrOldVersion, report.EntryError) + } + if MaxVersion.LessThan(*tv) { + return report.ReportFromError(ErrNewVersion, report.EntryError) + } + return report.Report{} +} diff --git a/config/v2_2/types/link.go b/config/v2_2/types/link.go new file mode 100644 index 000000000..1b7794c0d --- /dev/null +++ b/config/v2_2/types/link.go @@ -0,0 +1,35 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +func (s Link) Validate() report.Report { + r := report.Report{} + if !s.Hard { + err := validatePath(s.Target) + if err != nil { + r.Add(report.Entry{ + Message: fmt.Sprintf("problem with target path %q: %v", s.Target, err), + Kind: report.EntryError, + }) + } + } + return r +} diff --git a/config/v2_2/types/mode.go b/config/v2_2/types/mode.go new file mode 100644 index 000000000..f861a39b7 --- /dev/null +++ b/config/v2_2/types/mode.go @@ -0,0 +1,30 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "errors" +) + +var ( + ErrFileIllegalMode = errors.New("illegal file mode") +) + +func validateMode(m *int) error { + if m != nil && (*m < 0 || *m > 07777) { + return ErrFileIllegalMode + } + return nil +} diff --git a/config/v2_2/types/mode_test.go b/config/v2_2/types/mode_test.go new file mode 100644 index 000000000..c995565c3 --- /dev/null +++ b/config/v2_2/types/mode_test.go @@ -0,0 +1,66 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "reflect" + "testing" +) + +func TestModeValidate(t *testing.T) { + type in struct { + mode *int + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{mode: nil}, + out: out{}, + }, + { + in: in{mode: intToPtr(0)}, + out: out{}, + }, + { + in: in{mode: intToPtr(0644)}, + out: out{}, + }, + { + in: in{mode: intToPtr(01755)}, + out: out{}, + }, + { + in: in{mode: intToPtr(07777)}, + out: out{}, + }, + { + in: in{mode: intToPtr(010000)}, + out: out{ErrFileIllegalMode}, + }, + } + + for i, test := range tests { + err := validateMode(test.in.mode) + if !reflect.DeepEqual(test.out.err, err) { + t.Errorf("#%d: bad err: want %v, got %v", i, test.out.err, err) + } + } +} diff --git a/config/v2_2/types/node.go b/config/v2_2/types/node.go new file mode 100644 index 000000000..5c117f067 --- /dev/null +++ b/config/v2_2/types/node.go @@ -0,0 +1,78 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "path/filepath" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrNoFilesystem = errors.New("no filesystem specified") + ErrBothIDAndNameSet = errors.New("cannot set both id and name") +) + +func (n Node) ValidateFilesystem() report.Report { + r := report.Report{} + if n.Filesystem == "" { + r.Add(report.Entry{ + Message: ErrNoFilesystem.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (n Node) ValidatePath() report.Report { + r := report.Report{} + if err := validatePath(n.Path); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (n Node) Depth() int { + count := 0 + for p := filepath.Clean(string(n.Path)); p != "/"; count++ { + p = filepath.Dir(p) + } + return count +} + +func (nu NodeUser) Validate() report.Report { + r := report.Report{} + if nu.ID != nil && nu.Name != "" { + r.Add(report.Entry{ + Message: ErrBothIDAndNameSet.Error(), + Kind: report.EntryError, + }) + } + return r +} +func (ng NodeGroup) Validate() report.Report { + r := report.Report{} + if ng.ID != nil && ng.Name != "" { + r.Add(report.Entry{ + Message: ErrBothIDAndNameSet.Error(), + Kind: report.EntryError, + }) + } + return r +} diff --git a/config/v2_2/types/node_test.go b/config/v2_2/types/node_test.go new file mode 100644 index 000000000..6ecce8d37 --- /dev/null +++ b/config/v2_2/types/node_test.go @@ -0,0 +1,117 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/config/validate/report" +) + +func TestNodeValidatePath(t *testing.T) { + node := Node{Path: "not/absolute"} + rep := report.ReportFromError(ErrPathRelative, report.EntryError) + if receivedRep := node.ValidatePath(); !reflect.DeepEqual(rep, receivedRep) { + t.Errorf("bad error: want %v, got %v", rep, receivedRep) + } +} + +func TestNodeValidateFilesystem(t *testing.T) { + tests := []struct { + node Node + r report.Report + }{ + { + node: Node{Filesystem: "foo", Path: "/"}, + r: report.Report{}, + }, + { + node: Node{Path: "/"}, + r: report.ReportFromError(ErrNoFilesystem, report.EntryError), + }, + } + for i, test := range tests { + if receivedRep := test.node.ValidateFilesystem(); !reflect.DeepEqual(test.r, receivedRep) { + t.Errorf("#%d: bad error: want %v, got %v", i, test.r, receivedRep) + } + } +} + +func intToPtr(x int) *int { + return &x +} + +func TestNodeValidateUser(t *testing.T) { + tests := []struct { + in NodeUser + out report.Report + }{ + { + in: NodeUser{intToPtr(0), ""}, + out: report.Report{}, + }, + { + in: NodeUser{intToPtr(1000), ""}, + out: report.Report{}, + }, + { + in: NodeUser{nil, "core"}, + out: report.Report{}, + }, + { + in: NodeUser{intToPtr(1000), "core"}, + out: report.ReportFromError(ErrBothIDAndNameSet, report.EntryError), + }, + } + + for i, test := range tests { + report := test.in.Validate() + if !reflect.DeepEqual(test.out, report) { + t.Errorf("#%d: bad report: want %v got %v", i, test.out, report) + } + } +} + +func TestNodeValidateGroup(t *testing.T) { + tests := []struct { + in NodeGroup + out report.Report + }{ + { + in: NodeGroup{intToPtr(0), ""}, + out: report.Report{}, + }, + { + in: NodeGroup{intToPtr(1000), ""}, + out: report.Report{}, + }, + { + in: NodeGroup{nil, "core"}, + out: report.Report{}, + }, + { + in: NodeGroup{intToPtr(1000), "core"}, + out: report.ReportFromError(ErrBothIDAndNameSet, report.EntryError), + }, + } + + for i, test := range tests { + report := test.in.Validate() + if !reflect.DeepEqual(test.out, report) { + t.Errorf("#%d: bad report: want %v got %v", i, test.out, report) + } + } +} diff --git a/config/v2_2/types/partition.go b/config/v2_2/types/partition.go new file mode 100644 index 000000000..2d44defb6 --- /dev/null +++ b/config/v2_2/types/partition.go @@ -0,0 +1,83 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/coreos/ignition/config/validate/report" +) + +const ( + guidRegexStr = "^(|[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})$" +) + +var ( + ErrLabelTooLong = errors.New("partition labels may not exceed 36 characters") + ErrDoesntMatchGUIDRegex = errors.New("doesn't match the form \"01234567-89AB-CDEF-EDCB-A98765432101\"") + ErrLabelContainsColon = errors.New("partition label will be truncated to text before the colon") +) + +func (p Partition) ValidateLabel() report.Report { + r := 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(p.Label) > 36 { + r.Add(report.Entry{ + Message: ErrLabelTooLong.Error(), + Kind: report.EntryError, + }) + } + + // sgdisk uses colons for delimitting compound arguments and does not allow escaping them. + if strings.Contains(p.Label, ":") { + r.Add(report.Entry{ + Message: ErrLabelContainsColon.Error(), + Kind: report.EntryWarning, + }) + } + return r +} + +func (p Partition) ValidateTypeGUID() report.Report { + return validateGUID(p.TypeGUID) +} + +func (p Partition) ValidateGUID() report.Report { + return validateGUID(p.GUID) +} + +func validateGUID(guid string) report.Report { + r := report.Report{} + ok, err := regexp.MatchString(guidRegexStr, guid) + if err != nil { + r.Add(report.Entry{ + Message: fmt.Sprintf("error matching guid regexp: %v", err), + Kind: report.EntryError, + }) + } else if !ok { + r.Add(report.Entry{ + Message: ErrDoesntMatchGUIDRegex.Error(), + Kind: report.EntryError, + }) + } + return r +} diff --git a/config/v2_2/types/partition_test.go b/config/v2_2/types/partition_test.go new file mode 100644 index 000000000..a3994146b --- /dev/null +++ b/config/v2_2/types/partition_test.go @@ -0,0 +1,126 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/config/validate/report" +) + +func TestValidateLabel(t *testing.T) { + type in struct { + label string + } + type out struct { + report report.Report + } + tests := []struct { + in in + out out + }{ + { + in{"root"}, + out{report.Report{}}, + }, + { + in{""}, + out{report.Report{}}, + }, + { + in{"111111111111111111111111111111111111"}, + out{report.Report{}}, + }, + { + in{"1111111111111111111111111111111111111"}, + out{report.ReportFromError(ErrLabelTooLong, report.EntryError)}, + }, + { + in{"test:"}, + out{report.ReportFromError(ErrLabelContainsColon, report.EntryWarning)}, + }, + } + for i, test := range tests { + r := Partition{Label: test.in.label}.ValidateLabel() + if !reflect.DeepEqual(r, test.out.report) { + t.Errorf("#%d: wanted %v, got %v", i, test.out.report, r) + } + } +} + +func TestValidateTypeGUID(t *testing.T) { + type in struct { + typeguid string + } + type out struct { + report report.Report + } + tests := []struct { + in in + out out + }{ + { + in{"5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6"}, + out{report.Report{}}, + }, + { + in{""}, + out{report.Report{}}, + }, + { + in{"not-a-valid-typeguid"}, + out{report.ReportFromError(ErrDoesntMatchGUIDRegex, report.EntryError)}, + }, + } + for i, test := range tests { + r := Partition{TypeGUID: test.in.typeguid}.ValidateTypeGUID() + if !reflect.DeepEqual(r, test.out.report) { + t.Errorf("#%d: wanted %v, got %v", i, test.out.report, r) + } + } +} + +func TestValidateGUID(t *testing.T) { + type in struct { + guid string + } + type out struct { + report report.Report + } + tests := []struct { + in in + out out + }{ + { + in{"5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6"}, + out{report.Report{}}, + }, + { + in{""}, + out{report.Report{}}, + }, + { + in{"not-a-valid-typeguid"}, + out{report.ReportFromError(ErrDoesntMatchGUIDRegex, report.EntryError)}, + }, + } + for i, test := range tests { + r := Partition{GUID: test.in.guid}.ValidateGUID() + if !reflect.DeepEqual(r, test.out.report) { + t.Errorf("#%d: wanted %v, got %v", i, test.out.report, r) + } + } +} diff --git a/config/v2_2/types/passwd.go b/config/v2_2/types/passwd.go new file mode 100644 index 000000000..5f7d96415 --- /dev/null +++ b/config/v2_2/types/passwd.go @@ -0,0 +1,82 @@ +// Copyright 2017 CoreOS, 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 types + +import ( + "errors" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrPasswdCreateDeprecated = errors.New("the create object has been deprecated in favor of user-level options") + ErrPasswdCreateAndGecos = errors.New("cannot use both the create object and the user-level gecos field") + ErrPasswdCreateAndGroups = errors.New("cannot use both the create object and the user-level groups field") + ErrPasswdCreateAndHomeDir = errors.New("cannot use both the create object and the user-level homeDir field") + ErrPasswdCreateAndNoCreateHome = errors.New("cannot use both the create object and the user-level noCreateHome field") + ErrPasswdCreateAndNoLogInit = errors.New("cannot use both the create object and the user-level noLogInit field") + ErrPasswdCreateAndNoUserGroup = errors.New("cannot use both the create object and the user-level noUserGroup field") + ErrPasswdCreateAndPrimaryGroup = errors.New("cannot use both the create object and the user-level primaryGroup field") + ErrPasswdCreateAndShell = errors.New("cannot use both the create object and the user-level shell field") + ErrPasswdCreateAndSystem = errors.New("cannot use both the create object and the user-level system field") + ErrPasswdCreateAndUID = errors.New("cannot use both the create object and the user-level uid field") +) + +func (p PasswdUser) Validate() report.Report { + r := report.Report{} + if p.Create != nil { + r.Add(report.Entry{ + Message: ErrPasswdCreateDeprecated.Error(), + Kind: report.EntryWarning, + }) + addErr := func(err error) { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + if p.Gecos != "" { + addErr(ErrPasswdCreateAndGecos) + } + if len(p.Groups) > 0 { + addErr(ErrPasswdCreateAndGroups) + } + if p.HomeDir != "" { + addErr(ErrPasswdCreateAndHomeDir) + } + if p.NoCreateHome { + addErr(ErrPasswdCreateAndNoCreateHome) + } + if p.NoLogInit { + addErr(ErrPasswdCreateAndNoLogInit) + } + if p.NoUserGroup { + addErr(ErrPasswdCreateAndNoUserGroup) + } + if p.PrimaryGroup != "" { + addErr(ErrPasswdCreateAndPrimaryGroup) + } + if p.Shell != "" { + addErr(ErrPasswdCreateAndShell) + } + if p.System { + addErr(ErrPasswdCreateAndSystem) + } + if p.UID != nil { + addErr(ErrPasswdCreateAndUID) + } + } + return r +} diff --git a/config/v2_2/types/path.go b/config/v2_2/types/path.go new file mode 100644 index 000000000..0bdbdcb00 --- /dev/null +++ b/config/v2_2/types/path.go @@ -0,0 +1,31 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "path" +) + +var ( + ErrPathRelative = errors.New("path not absolute") +) + +func validatePath(p string) error { + if !path.IsAbs(p) { + return ErrPathRelative + } + return nil +} diff --git a/config/v2_2/types/path_test.go b/config/v2_2/types/path_test.go new file mode 100644 index 000000000..ed3805eab --- /dev/null +++ b/config/v2_2/types/path_test.go @@ -0,0 +1,62 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "reflect" + "testing" +) + +func TestPathValidate(t *testing.T) { + type in struct { + device string + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{device: "/good/path"}, + out: out{}, + }, + { + in: in{device: "/name"}, + out: out{}, + }, + { + in: in{device: "/this/is/a/fairly/long/path/to/a/device."}, + out: out{}, + }, + { + in: in{device: "/this one has spaces"}, + out: out{}, + }, + { + in: in{device: "relative/path"}, + out: out{err: ErrPathRelative}, + }, + } + + for i, test := range tests { + err := validatePath(test.in.device) + if !reflect.DeepEqual(test.out.err, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) + } + } +} diff --git a/config/v2_2/types/raid.go b/config/v2_2/types/raid.go new file mode 100644 index 000000000..f43b152d5 --- /dev/null +++ b/config/v2_2/types/raid.go @@ -0,0 +1,58 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "fmt" + + "github.com/coreos/ignition/config/validate/report" +) + +func (n Raid) ValidateLevel() report.Report { + r := report.Report{} + switch n.Level { + case "linear", "raid0", "0", "stripe": + if n.Spares != 0 { + r.Add(report.Entry{ + Message: fmt.Sprintf("spares unsupported for %q arrays", n.Level), + Kind: report.EntryError, + }) + } + case "raid1", "1", "mirror": + case "raid4", "4": + case "raid5", "5": + case "raid6", "6": + case "raid10", "10": + default: + r.Add(report.Entry{ + Message: fmt.Sprintf("unrecognized raid level: %q", n.Level), + Kind: report.EntryError, + }) + } + return r +} + +func (n Raid) ValidateDevices() report.Report { + r := report.Report{} + for _, d := range n.Devices { + if err := validatePath(string(d)); err != nil { + r.Add(report.Entry{ + Message: fmt.Sprintf("array %q: device path not absolute: %q", n.Name, d), + Kind: report.EntryError, + }) + } + } + return r +} diff --git a/config/v2_2/types/schema.go b/config/v2_2/types/schema.go new file mode 100644 index 000000000..4b32b337b --- /dev/null +++ b/config/v2_2/types/schema.go @@ -0,0 +1,246 @@ +package types + +// generated by "schematyper --package=types schema/ignition.json -o config/types/schema.go --root-type=Config" -- DO NOT EDIT + +type CaReference struct { + Source string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +type Config struct { + Ignition Ignition `json:"ignition"` + Networkd Networkd `json:"networkd,omitempty"` + Passwd Passwd `json:"passwd,omitempty"` + Storage Storage `json:"storage,omitempty"` + Systemd Systemd `json:"systemd,omitempty"` +} + +type ConfigReference struct { + Source string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +type Create struct { + Force bool `json:"force,omitempty"` + Options []CreateOption `json:"options,omitempty"` +} + +type CreateOption string + +type Device string + +type Directory struct { + Node + DirectoryEmbedded1 +} + +type DirectoryEmbedded1 struct { + Mode *int `json:"mode,omitempty"` +} + +type Disk struct { + Device string `json:"device,omitempty"` + Partitions []Partition `json:"partitions,omitempty"` + WipeTable bool `json:"wipeTable,omitempty"` +} + +type File struct { + Node + FileEmbedded1 +} + +type FileContents struct { + Compression string `json:"compression,omitempty"` + Source string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +type FileEmbedded1 struct { + Append bool `json:"append,omitempty"` + Contents FileContents `json:"contents,omitempty"` + Mode *int `json:"mode,omitempty"` +} + +type Filesystem struct { + Mount *Mount `json:"mount,omitempty"` + Name string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +type Group string + +type Ignition struct { + Config IgnitionConfig `json:"config,omitempty"` + Security Security `json:"security,omitempty"` + Timeouts Timeouts `json:"timeouts,omitempty"` + Version string `json:"version,omitempty"` +} + +type IgnitionConfig struct { + Append []ConfigReference `json:"append,omitempty"` + Replace *ConfigReference `json:"replace,omitempty"` +} + +type Link struct { + Node + LinkEmbedded1 +} + +type LinkEmbedded1 struct { + Hard bool `json:"hard,omitempty"` + Target string `json:"target,omitempty"` +} + +type Mount struct { + Create *Create `json:"create,omitempty"` + Device string `json:"device,omitempty"` + Format string `json:"format,omitempty"` + Label *string `json:"label,omitempty"` + Options []MountOption `json:"options,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeFilesystem bool `json:"wipeFilesystem,omitempty"` +} + +type MountOption string + +type Networkd struct { + Units []Networkdunit `json:"units,omitempty"` +} + +type NetworkdDropin struct { + Contents string `json:"contents,omitempty"` + Name string `json:"name,omitempty"` +} + +type Networkdunit struct { + Contents string `json:"contents,omitempty"` + Dropins []NetworkdDropin `json:"dropins,omitempty"` + Name string `json:"name,omitempty"` +} + +type Node struct { + Filesystem string `json:"filesystem,omitempty"` + Group *NodeGroup `json:"group,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` + Path string `json:"path,omitempty"` + User *NodeUser `json:"user,omitempty"` +} + +type NodeGroup struct { + ID *int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +type NodeUser struct { + ID *int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +type Partition struct { + GUID string `json:"guid,omitempty"` + Label string `json:"label,omitempty"` + Number int `json:"number,omitempty"` + Size int `json:"size,omitempty"` + Start int `json:"start,omitempty"` + TypeGUID string `json:"typeGuid,omitempty"` +} + +type Passwd struct { + Groups []PasswdGroup `json:"groups,omitempty"` + Users []PasswdUser `json:"users,omitempty"` +} + +type PasswdGroup struct { + Gid *int `json:"gid,omitempty"` + Name string `json:"name,omitempty"` + PasswordHash string `json:"passwordHash,omitempty"` + System bool `json:"system,omitempty"` +} + +type PasswdUser struct { + Create *Usercreate `json:"create,omitempty"` + Gecos string `json:"gecos,omitempty"` + Groups []Group `json:"groups,omitempty"` + HomeDir string `json:"homeDir,omitempty"` + Name string `json:"name,omitempty"` + NoCreateHome bool `json:"noCreateHome,omitempty"` + NoLogInit bool `json:"noLogInit,omitempty"` + NoUserGroup bool `json:"noUserGroup,omitempty"` + PasswordHash *string `json:"passwordHash,omitempty"` + PrimaryGroup string `json:"primaryGroup,omitempty"` + SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"` + Shell string `json:"shell,omitempty"` + System bool `json:"system,omitempty"` + UID *int `json:"uid,omitempty"` +} + +type Raid struct { + Devices []Device `json:"devices,omitempty"` + Level string `json:"level,omitempty"` + Name string `json:"name,omitempty"` + Options []RaidOption `json:"options,omitempty"` + Spares int `json:"spares,omitempty"` +} + +type RaidOption string + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `json:"tls,omitempty"` +} + +type Storage struct { + Directories []Directory `json:"directories,omitempty"` + Disks []Disk `json:"disks,omitempty"` + Files []File `json:"files,omitempty"` + Filesystems []Filesystem `json:"filesystems,omitempty"` + Links []Link `json:"links,omitempty"` + Raid []Raid `json:"raid,omitempty"` +} + +type Systemd struct { + Units []Unit `json:"units,omitempty"` +} + +type SystemdDropin struct { + Contents string `json:"contents,omitempty"` + Name string `json:"name,omitempty"` +} + +type TLS struct { + CertificateAuthorities []CaReference `json:"certificateAuthorities,omitempty"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"` + HTTPTotal *int `json:"httpTotal,omitempty"` +} + +type Unit struct { + Contents string `json:"contents,omitempty"` + Dropins []SystemdDropin `json:"dropins,omitempty"` + Enable bool `json:"enable,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Mask bool `json:"mask,omitempty"` + Name string `json:"name,omitempty"` +} + +type Usercreate struct { + Gecos string `json:"gecos,omitempty"` + Groups []UsercreateGroup `json:"groups,omitempty"` + HomeDir string `json:"homeDir,omitempty"` + NoCreateHome bool `json:"noCreateHome,omitempty"` + NoLogInit bool `json:"noLogInit,omitempty"` + NoUserGroup bool `json:"noUserGroup,omitempty"` + PrimaryGroup string `json:"primaryGroup,omitempty"` + Shell string `json:"shell,omitempty"` + System bool `json:"system,omitempty"` + UID *int `json:"uid,omitempty"` +} + +type UsercreateGroup string + +type Verification struct { + Hash *string `json:"hash,omitempty"` +} diff --git a/config/v2_2/types/unit.go b/config/v2_2/types/unit.go new file mode 100644 index 000000000..7ad9fed58 --- /dev/null +++ b/config/v2_2/types/unit.go @@ -0,0 +1,131 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "bytes" + "errors" + "fmt" + "path" + + "github.com/coreos/go-systemd/unit" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrInvalidSystemdExt = errors.New("invalid systemd unit extension") + ErrInvalidNetworkdExt = errors.New("invalid networkd unit extension") +) + +func (u Unit) ValidateContents() report.Report { + r := report.Report{} + if err := validateUnitContent(u.Contents); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (u Unit) ValidateName() report.Report { + r := report.Report{} + switch path.Ext(u.Name) { + case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope": + default: + r.Add(report.Entry{ + Message: ErrInvalidSystemdExt.Error(), + Kind: report.EntryError, + }) + } + return r +} + +func (d SystemdDropin) Validate() report.Report { + r := report.Report{} + + if err := validateUnitContent(d.Contents); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + + switch path.Ext(d.Name) { + case ".conf": + default: + r.Add(report.Entry{ + Message: fmt.Sprintf("invalid systemd unit drop-in extension: %q", path.Ext(d.Name)), + Kind: report.EntryError, + }) + } + + return r +} + +func (u Networkdunit) Validate() report.Report { + r := report.Report{} + + if err := validateUnitContent(u.Contents); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + + switch path.Ext(u.Name) { + case ".link", ".netdev", ".network": + default: + r.Add(report.Entry{ + Message: ErrInvalidNetworkdExt.Error(), + Kind: report.EntryError, + }) + } + + return r +} + +func (d NetworkdDropin) Validate() report.Report { + r := report.Report{} + + if err := validateUnitContent(d.Contents); err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + } + + switch path.Ext(d.Name) { + case ".conf": + default: + r.Add(report.Entry{ + Message: fmt.Sprintf("invalid networkd unit drop-in extension: %q", path.Ext(d.Name)), + Kind: report.EntryError, + }) + } + + return r +} + +func validateUnitContent(content string) error { + c := bytes.NewBufferString(content) + _, err := unit.Deserialize(c) + if err != nil { + return fmt.Errorf("invalid unit content: %s", err) + } + + return nil +} diff --git a/config/v2_2/types/unit_test.go b/config/v2_2/types/unit_test.go new file mode 100644 index 000000000..147ff82f8 --- /dev/null +++ b/config/v2_2/types/unit_test.go @@ -0,0 +1,219 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "reflect" + "testing" + + "github.com/coreos/ignition/config/validate/report" +) + +func TestSystemdUnitValidateContents(t *testing.T) { + type in struct { + unit Unit + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: Unit{Name: "test.service", Contents: "[Foo]\nQux=Bar"}}, + out: out{err: nil}, + }, + { + in: in{unit: Unit{Name: "test.service", Contents: "[Foo"}}, + out: out{err: errors.New("invalid unit content: unable to find end of section")}, + }, + { + in: in{unit: Unit{Name: "test.service", Contents: "", Dropins: []SystemdDropin{{}}}}, + out: out{err: nil}, + }, + } + + for i, test := range tests { + err := test.in.unit.ValidateContents() + 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 TestSystemdUnitValidateName(t *testing.T) { + type in struct { + unit string + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: "test.service"}, + out: out{err: nil}, + }, + { + in: in{unit: "test.socket"}, + out: out{err: nil}, + }, + { + in: in{unit: "test.blah"}, + out: out{err: ErrInvalidSystemdExt}, + }, + } + + for i, test := range tests { + err := Unit{Name: test.in.unit, Contents: "[Foo]\nQux=Bar"}.ValidateName() + 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 TestSystemdUnitDropInValidate(t *testing.T) { + type in struct { + unit SystemdDropin + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: SystemdDropin{Name: "test.conf", Contents: "[Foo]\nQux=Bar"}}, + out: out{err: nil}, + }, + { + in: in{unit: SystemdDropin{Name: "test.conf", Contents: "[Foo"}}, + out: out{err: errors.New("invalid unit content: unable to find end of section")}, + }, + } + + 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 TestNetworkdUnitNameValidate(t *testing.T) { + type in struct { + unit string + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: "test.network"}, + out: out{err: nil}, + }, + { + in: in{unit: "test.link"}, + out: out{err: nil}, + }, + { + in: in{unit: "test.netdev"}, + out: out{err: nil}, + }, + { + in: in{unit: "test.blah"}, + out: out{err: ErrInvalidNetworkdExt}, + }, + } + + for i, test := range tests { + err := Networkdunit{Name: test.in.unit, Contents: "[Foo]\nQux=Bar"}.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 TestNetworkdUnitValidate(t *testing.T) { + type in struct { + unit Networkdunit + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: Networkdunit{Name: "test.network", Contents: "[Foo]\nQux=Bar"}}, + out: out{err: nil}, + }, + { + in: in{unit: Networkdunit{Name: "test.network", Contents: "[Foo"}}, + out: out{err: errors.New("invalid unit content: unable to find end of section")}, + }, + } + + 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 TestNetworkdUnitDropInValidate(t *testing.T) { + type in struct { + unit NetworkdDropin + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{unit: NetworkdDropin{Name: "test.conf", Contents: "[Foo]\nQux=Bar"}}, + out: out{err: nil}, + }, + { + in: in{unit: NetworkdDropin{Name: "test.conf", Contents: "[Foo"}}, + out: out{err: errors.New("invalid unit content: unable to find end of section")}, + }, + } + + 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) + } + } +} diff --git a/config/v2_2/types/url.go b/config/v2_2/types/url.go new file mode 100644 index 000000000..f6270c9a2 --- /dev/null +++ b/config/v2_2/types/url.go @@ -0,0 +1,49 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "errors" + "net/url" + + "github.com/vincent-petithory/dataurl" +) + +var ( + ErrInvalidScheme = errors.New("invalid url scheme") +) + +func validateURL(s string) error { + // Empty url is valid, indicates an empty file + if s == "" { + return nil + } + u, err := url.Parse(s) + if err != nil { + return err + } + + switch u.Scheme { + case "http", "https", "oem", "tftp", "s3": + return nil + case "data": + if _, err := dataurl.DecodeString(s); err != nil { + return err + } + return nil + default: + return ErrInvalidScheme + } +} diff --git a/config/v2_2/types/url_test.go b/config/v2_2/types/url_test.go new file mode 100644 index 000000000..5b23a7af7 --- /dev/null +++ b/config/v2_2/types/url_test.go @@ -0,0 +1,70 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "reflect" + "testing" +) + +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: "tftp://example.com:69/foobar.txt"}, + out: out{}, + }, + { + in: in{u: "data:,example%20file%0A"}, + out: out{}, + }, + { + in: in{u: "bad://"}, + out: out{err: ErrInvalidScheme}, + }, + } + + for i, test := range tests { + err := validateURL(test.in.u) + if !reflect.DeepEqual(test.out.err, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) + } + } +} diff --git a/config/v2_2/types/verification.go b/config/v2_2/types/verification.go new file mode 100644 index 000000000..d2158b808 --- /dev/null +++ b/config/v2_2/types/verification.go @@ -0,0 +1,83 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "crypto" + "encoding/hex" + "errors" + "strings" + + "github.com/coreos/ignition/config/validate/report" +) + +var ( + ErrHashMalformed = errors.New("malformed hash specifier") + ErrHashWrongSize = errors.New("incorrect size for hash sum") + ErrHashUnrecognized = errors.New("unrecognized hash function") +) + +// HashParts will return the sum and function (in that order) of the hash stored +// in this Verification, or an error if there is an issue during parsing. +func (v Verification) HashParts() (string, string, error) { + if v.Hash == nil { + // The hash can be nil + return "", "", nil + } + parts := strings.SplitN(*v.Hash, "-", 2) + if len(parts) != 2 { + return "", "", ErrHashMalformed + } + + return parts[0], parts[1], nil +} + +func (v Verification) Validate() report.Report { + r := report.Report{} + + if v.Hash == nil { + // The hash can be nil + return r + } + + function, sum, err := v.HashParts() + if err != nil { + r.Add(report.Entry{ + Message: err.Error(), + Kind: report.EntryError, + }) + return r + } + var hash crypto.Hash + switch function { + case "sha512": + hash = crypto.SHA512 + default: + r.Add(report.Entry{ + Message: ErrHashUnrecognized.Error(), + Kind: report.EntryError, + }) + return r + } + + if len(sum) != hex.EncodedLen(hash.Size()) { + r.Add(report.Entry{ + Message: ErrHashWrongSize.Error(), + Kind: report.EntryError, + }) + } + + return r +} diff --git a/config/v2_2/types/verification_test.go b/config/v2_2/types/verification_test.go new file mode 100644 index 000000000..2afb45b0a --- /dev/null +++ b/config/v2_2/types/verification_test.go @@ -0,0 +1,92 @@ +// Copyright 2016 CoreOS, 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 types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/config/validate/report" +) + +func TestHashParts(t *testing.T) { + type in struct { + data string + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{data: `"sha512-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"`}, + }, + { + in: in{data: `"sha512:01234567"`}, + out: out{err: ErrHashMalformed}, + }, + } + + for i, test := range tests { + fun, sum, err := Verification{Hash: &test.in.data}.HashParts() + if err != test.out.err { + t.Fatalf("#%d: bad error: want %+v, got %+v", i, test.out.err, err) + } + if err == nil && fun+"-"+sum != test.in.data { + t.Fatalf("#%d: bad hash: want %+v, got %+v", i, test.in.data, fun+"-"+sum) + } + } +} + +func TestHashValidate(t *testing.T) { + type in struct { + v Verification + } + type out struct { + err error + } + + h1 := "xor-abcdef" + h2 := "sha512-123" + h3 := "sha512-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + tests := []struct { + in in + out out + }{ + { + in: in{v: Verification{Hash: &h1}}, + out: out{err: ErrHashUnrecognized}, + }, + { + in: in{v: Verification{Hash: &h2}}, + out: out{err: ErrHashWrongSize}, + }, + { + in: in{v: Verification{Hash: &h3}}, + out: out{}, + }, + } + + for i, test := range tests { + err := test.in.v.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) + } + } +} diff --git a/config/validate/validate_test.go b/config/validate/validate_test.go index 6fa608550..421b5d5ed 100644 --- a/config/validate/validate_test.go +++ b/config/validate/validate_test.go @@ -55,7 +55,7 @@ func TestValidate(t *testing.T) { out: out{err: ErrInvalidVersion}, }, { - in: in{cfg: Config{Ignition: Ignition{Version: "2.2.0"}}}, + in: in{cfg: Config{Ignition: Ignition{Version: "2.3.0"}}}, out: out{err: ErrNewVersion}, }, { diff --git a/doc/configuration-v2_2.md b/doc/configuration-v2_2.md new file mode 100644 index 000000000..8401822a4 --- /dev/null +++ b/doc/configuration-v2_2.md @@ -0,0 +1,143 @@ +# Configuration Specification v2.2.0 # + +The Ignition configuration is a JSON document conforming to the following specification, with **_italicized_** entries being optional: + +* **ignition** (object): metadata about the configuration itself. + * **version** (string): the semantic version number of the spec. The spec version must be compatible with the latest version (`2.2.0`). Compatibility requires the major versions to match and the spec version be less than or equal to the latest version. `-experimental` versions compare less than the final version with the same number, and previous experimental versions are not accepted. + * **_config_** (objects): options related to the configuration. + * **_append_** (list of objects): a list of the configs to be appended to the current config. + * **source** (string): the URL of the config. Supported schemes are `http`, `https`, `s3`, `tftp`, and [`data`][rfc2397]. Note: When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is `sha512`. + * **_replace_** (object): the config that will replace the current. + * **source** (string): the URL of the config. Supported schemes are `http`, `https`, `s3`, `tftp`, and [`data`][rfc2397]. Note: When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is `sha512`. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_httpResponseHeaders_** (integer) the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_httpTotal_** (integer) the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificateAuthorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. + * **source** (string): the URL of the certificate (in PEM format). Supported schemes are `http`, `https`, `s3`, `tftp`, and [`data`][rfc2397]. Note: When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. + * **_verification_** (object): options related to the verification of the certificate. + * **_hash_** (string): the hash of the certificate, in the form `-` where type is sha512. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipeTable_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates it's position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_** (integer): the size of the partition (in device logical sectors, 512 or 4096 bytes). If zero, the partition will be made as large as possible. + * **_start_** (integer): the start of the partition (in device logical sectors). If zero, the partition will be positioned at the start of the largest block available. + * **_typeGuid_** (string): the GPT [partition type GUID][part-types]. If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_raid_** (list of objects): the list of RAID arrays to be configured. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured and/or used in the "files" section. Either "mount" or "path" needs to be specified. + * **_name_** (string): the identifier for the filesystem, internal to Ignition. This is only required if the filesystem needs to be referenced in the "files" section. + * **_mount_** (object): contains the set of mount and formatting options for the filesystem. A non-null entry indicates that the filesystem should be mounted before it is used by Ignition. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, or swap). + * **_wipeFilesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [the documentation on filesystems](operator-notes.md#filesystem-reuse-semantics) for more information. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_create_** (object, DEPRECATED): contains the set of options to be used when creating the filesystem. + * **_force_** (boolean, DEPRECATED): whether or not the create operation shall overwrite an existing filesystem. + * **_options_** (list of strings, DEPRECATED): any additional options to be passed to the format-specific mkfs utility. + * **_path_** (string): the mount-point of the filesystem. A non-null entry indicates that the filesystem has already been mounted by the system at the specified path. This is really only useful for "/sysroot". + * **_files_** (list of objects): the list of files to be written. + * **filesystem** (string): the internal identifier of the filesystem in which to write the file. This matches the last filesystem with the given identifier. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. Defaults to true. + * **_append_** (boolean): whether to append to the specified file. Creates a new file if nothing exists at the path. Cannot be set if overwrite is set to true. + * **_contents_** (object): options related to the contents of the file. + * **_compression_** (string): the type of compression used on the contents (null or gzip). Compression cannot be used with S3. + * **_source_** (string): the URL of the file contents. Supported schemes are `http`, `https`, `tftp`, `s3`, and [`data`][rfc2397]. When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. + * **_verification_** (object): options related to the verification of the file contents. + * **_hash_** (string): the hash of the config, in the form `-` where type is `sha512`. + * **_mode_** (integer): the file's permission mode. Note that the mode must be properly specified as a **decimal** value (i.e. 0644 -> 420). + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group of the owner. + * **_id_** (integer): the group ID of the owner. + * **_name_** (string): the group name of the owner. + * **_directories_** (list of objects): the list of directories to be created. + * **filesystem** (string): the internal identifier of the filesystem in which to create the directory. This matches the last filesystem with the given identifier. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. + * **_mode_** (integer): the directory's permission mode. Note that the mode must be properly specified as a **decimal** value (i.e. 0755 -> 493). + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group of the owner. + * **_id_** (integer): the group ID of the owner. + * **_name_** (string): the group name of the owner. + * **_links_** (list of objects): the list of links to be created + * **filesystem** (string): the internal identifier of the filesystem in which to write the link. This matches the last filesystem with the given identifier. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. + * **_user_** (object): specifies the symbolic link's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group of the owner. + * **_id_** (integer): the group ID of the owner. + * **_name_** (string): the group name of the owner. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enable_** (boolean, DEPRECATED): whether or not the service shall be enabled. When true, the service is enabled. In order for this to have any effect, the unit must have an install section. + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_networkd_** (object): describes the desired state of the networkd files. + * **_units_** (list of objects): the list of networkd files. + * **name** (string): the name of the file. This must be suffixed with a valid unit type (e.g. "00-eth0.network"). + * **_contents_** (string): the contents of the networkd file. + * **_dropins_** (list of objects): the list of drop-ins for the unit. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. + * **name** (string): the username for the account. + * **_passwordHash_** (string): the encrypted password for the account. + * **_sshAuthorizedKeys_** (list of strings): a list of SSH keys to be added to the user's authorized_keys. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_homeDir_** (string): the home directory of the account. + * **_noCreateHome_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primaryGroup_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_noUserGroup_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_noLogInit_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_system_** (bool): whether or not to make the account a system account. This only has an effect if the account doesn't exist yet. + * **_create_** (object, DEPRECATED): contains the set of options to be used when creating the user. A non-null entry indicates that the user account shall be created. This object has been marked for deprecation, please use the **_users_** level fields instead. + * **_uid_** (integer): the user ID of the new account. + * **_gecos_** (string): the GECOS field of the new account. + * **_homeDir_** (string): the home directory of the new account. + * **_noCreateHome_** (boolean): whether or not to create the user's home directory. + * **_primaryGroup_** (string): the name or ID of the primary group of the new account. + * **_groups_** (list of strings): the list of supplementary groups of the new account. + * **_noUserGroup_** (boolean): whether or not to create a group with the same name as the user. + * **_noLogInit_** (boolean): whether or not to add the user to the lastlog and faillog databases. + * **_shell_** (string): the login shell of the new account. + * **_groups_** (list of objects): the list of groups to be added. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_passwordHash_** (string): the encrypted password of the new group. + +[part-types]: http://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs +[rfc2397]: https://tools.ietf.org/html/rfc2397 diff --git a/doc/configuration-v2_2-experimental.md b/doc/configuration-v2_3-experimental.md similarity index 99% rename from doc/configuration-v2_2-experimental.md rename to doc/configuration-v2_3-experimental.md index 2a150c757..86d427940 100644 --- a/doc/configuration-v2_2-experimental.md +++ b/doc/configuration-v2_3-experimental.md @@ -1,11 +1,11 @@ -# Configuration Specification v2.2.0-experimental # +# Configuration Specification v2.3.0-experimental # *NOTE*: This pre-release version of the specification is experimental and is subject to change without notice or regard to backward compatibility. The Ignition configuration is a JSON document conforming to the following specification, with **_italicized_** entries being optional: * **ignition** (object): metadata about the configuration itself. - * **version** (string): the semantic version number of the spec. The spec version must be compatible with the latest version (`2.2.0-experimental`). Compatibility requires the major versions to match and the spec version be less than or equal to the latest version. `-experimental` versions compare less than the final version with the same number, and previous experimental versions are not accepted. + * **version** (string): the semantic version number of the spec. The spec version must be compatible with the latest version (`2.3.0-experimental`). Compatibility requires the major versions to match and the spec version be less than or equal to the latest version. `-experimental` versions compare less than the final version with the same number, and previous experimental versions are not accepted. * **_config_** (objects): options related to the configuration. * **_append_** (list of objects): a list of the configs to be appended to the current config. * **source** (string): the URL of the config. Supported schemes are `http`, `https`, `s3`, `tftp`, and [`data`][rfc2397]. Note: When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. diff --git a/doc/development.md b/doc/development.md index b3141925d..ea2024535 100644 --- a/doc/development.md +++ b/doc/development.md @@ -104,3 +104,34 @@ The test should be added to the init function inside of the test file. If the test module is being created then an `init` function should be created which registers the tests and the package must be imported inside of `tests/registry/registry.go` to allow for discovery. + +## Marking an experimental spec as stable + +When an experimental version of the Ignition config spec (e.g.: +`2.3.0-experimental`) is to be declared stable (e.g. `2.3.0`), there are a +handful of changes that must be made to the code base. These changes should have +the following effects: + +- Any configs with a `version` field set to the previously experimental version + will no longer pass validation. For example, if `2.3.0-experimental` is being + marked as stable, any configs written for `2.3.0-experimental` should have + their version fields changed to `2.3.0`, for Ignition will no longer accept + them. +- A new experimental spec version will be created. For example, if + `2.3.0-experimental` is being marked as stable, a new version of + `2.4.0-experimental` will now be accepted, and start to accumulate new changes + to the spec. +- Internally, any configs presented to Ignition will be translated into the new + experimental spec before Ignition begins processing them. For example, if the + new experimental spec is `2.4.0-experimental`, and Ignition is given a `2.3.0` + config, it will be converted into a `2.4.0-experimental` config before any + work is done. +- The new stable spec and the new experimental spec will be identical. The new + experimental spec is a direct copy of the old experimental spec, and no new + changes to the spec have been made yet, so initially the two specs will have + the same fields and semantics. +- The HTTP `user-agent` header that Ignition uses whenever fetching an object + and the HTTP `accept` header that Ignition uses whenever fetching a config + will be updated to advertise the new stable spec. +- New features will be documented in the [migrating + configs](doc/migrating-configs.md) documentation. diff --git a/doc/examples.md b/doc/examples.md index 33ad7609b..a83b3b08a 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -1,6 +1,6 @@ # Example Configs -Each of these examples is written in version 2.1.0 of the config. Ensure that any configuration is compatible with the version that Ignition accepts. Compatibility requires the major versions to match and the spec version be less than or equal to the version Ignition accepts. +Each of these examples is written in version 2.2.0 of the config. Ensure that any configuration is compatible with the version that Ignition accepts. Compatibility requires the major versions to match and the spec version be less than or equal to the version Ignition accepts. ## Services @@ -10,7 +10,7 @@ This config will write a single service unit (shown below) with the contents of ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "systemd": { "units": [{ "name": "example.service", @@ -38,7 +38,7 @@ This config will add a [systemd unit drop-in](https://coreos.com/os/docs/latest/ ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "systemd": { "units": [{ "name": "systemd-networkd.service", @@ -66,7 +66,7 @@ This example Ignition configuration will locate the device with the "ROOT" files ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "filesystems": [{ "mount": { @@ -86,7 +86,7 @@ This example Ignition configuration will locate the device with the "ROOT" files ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "filesystems": [{ "mount": { @@ -106,11 +106,12 @@ In many cases it is useful to write files to the root filesystem. This example w ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", "path": "/foo/bar", + "mode": 420, "contents": { "source": "data:,example%20file%0A" } }] } @@ -136,11 +137,12 @@ There are cases where it is desirable to write a file to disk, but with the cont ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", "path": "/foo/bar", + "mode": 420, "contents": { "source": "http://example.com/asset", "verification": { "hash": "sha512-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } @@ -158,7 +160,7 @@ In many scenarios, it may be useful to have an external data volume. This config ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "disks": [ { @@ -227,7 +229,7 @@ In some cloud environments, there is a limit on the size of the config which may ```json ignition { "ignition": { - "version": "2.1.0", + "version": "2.2.0", "config": { "replace": { "source": "http://example.com/config.json", @@ -246,7 +248,7 @@ Setting the hostname of a system is as simple as writing `/etc/hostname`: ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -264,7 +266,7 @@ Users can be added to an OS with the `passwd.users` key which takes a list of ob ```json ignition { - "ignition": { "version": "2.1.0" }, + "ignition": { "version": "2.2.0" }, "passwd": { "users": [ { diff --git a/doc/migrating-configs.md b/doc/migrating-configs.md index e108f845a..82b1dbca6 100644 --- a/doc/migrating-configs.md +++ b/doc/migrating-configs.md @@ -2,6 +2,162 @@ Occasionally, there are changes made to Ignition's configuration that break backward compatibility. While this is not a concern for running machines (since Ignition only runs one time during first boot), it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. +## From Version 2.1.0 to 2.2.0 + +There are not any breaking changes between versions 2.1.0 and versions 2.2.0 of the configuration specification. Any valid 2.1.0 configuration can be updated to a 2.2.0 configuration by simply changing the version string in the config. + +The 2.2.0 version of the configuration is greatly improved over version 2.1.0, with many new fields and behaviors added to the specification. + +The following is a list of notable new features, deprecations, and changes. + +### File appending + +The `files` section of the config has gained a new field called `append`. When this field is set to `true`, if there's a file at the path then the contents will be appended to the existing file. + +```json ignition +{ + "ignition": { "version": "2.2.0" }, + "storage": { + "files": [{ + "filesystem": "root", + "path": "/etc/hosts", + "append": true, + "mode": 420, + "contents": { + "source": "data:,10.0.0.2%20myname" + } + }] + } +} +``` + +### Node overwriting + +The `files`, `directories`, and `links` sections of the config have each gained a new field called `overwrite`. When this field is set to `true`, any preexisting nodes at the path of the thing to be created will be overwritten. This field defaults to `true` for files, and `false` for directories and links. + +```json ignition +{ + "ignition": { "version": "2.2.0" }, + "storage": { + "links": [{ + "filesystem": "root", + "path": "/etc/localtime", + "target": "/usr/share/zoneinfo/US/Pacific", + "overwrite": true + }] + } +} +``` + +### Custom RAID options + +The `raid` section has gained a new field called `options`, that allows arbitrary mdadm arguments to be specified. These arguments are passed directly on to mdadm when raid arrays are being created. + +```json ignition +{ + "ignition": { "version": "2.2.0" }, + "storage": { + "disks": [ + { + "device": "/dev/sdb", + "wipeTable": true, + "partitions": [{ + "label": "raid.1.1", + "number": 1, + "size": 20480, + "start": 0 + }] + }, + { + "device": "/dev/sdc", + "wipeTable": true, + "partitions": [{ + "label": "raid.1.2", + "number": 1, + "size": 20480, + "start": 0 + }] + } + ], + "raid": [{ + "devices": [ + "/dev/disk/by-partlabel/raid.1.1", + "/dev/disk/by-partlabel/raid.1.2" + ], + "level": "stripe", + "name": "data", + "options": [ + "--verbose" + ] + }], + "filesystems": [{ + "mount": { + "device": "/dev/md/data", + "format": "ext4", + "label": "DATA" + } + }] + }, + "systemd": { + "units": [{ + "name": "var-lib-data.mount", + "enable": true, + "contents": "[Mount]\nWhat=/dev/md/data\nWhere=/var/lib/data\nType=ext4\n\n[Install]\nWantedBy=local-fs.target" + }] + } +} +``` + +### Custom certificate authorities + +The `ignition` section has gained a new section named `security`, which can be used to specify custom certificate authorities to be used when fetching objects over `https`. These are used in addition to the system pool. These are not added to the system pool for the booted machine, and will only impact Ignition. + +```json ignition +{ + "ignition": { + "version": "2.2.0-experimental", + "config": { + "append": [{ + "source": "https://s3.com/securely-fetched-config.ign" + }] + }, + "security": { + "tls": { + "certificateAuthorities": [ + { + "source": "http://www.example.com/root.pem", + "verification": { + "hash": "sha512-ab800f66a7544c0a8dbed0c57b38a3c1487c3369e2e9e90704d0c07743557ab2a28c528720566ffc64e3dfd5df1a557a4979b33009f5fd493fea02a7e30041d2" + } + } + ] + } + } + } +} +``` + +### networkd dropins + +With the release of systemd v232, networkd dropins are now supported as a means of configuring existing networkd units. The `networkd` section has gained a `dropins` field to reflect this. + +```json ignition +{ + "ignition": { + "version": "2.2.0-experimental" + }, + "networkd": { + "units": [{ + "name": "zz-default.network", + "dropins": [{ + "name": "disable-dhcp.conf", + "contents": "data:,%5BNetwork%5D%0ADHCP%3Dno" + }] + }] + } +} +``` + ## From Version 2.0.0 to 2.1.0 There are not any breaking changes between versions 2.0.0 and versions 2.1.0 of the configuration specification. Any valid 2.0.0 configuration can be updated to a 2.1.0 configuration by simply changing the version string in the config. diff --git a/internal/resource/url.go b/internal/resource/url.go index 89204f732..8ab84ffa3 100644 --- a/internal/resource/url.go +++ b/internal/resource/url.go @@ -58,7 +58,7 @@ var ( // config is being fetched ConfigHeaders = http.Header{ "Accept-Encoding": []string{"identity"}, - "Accept": []string{"application/vnd.coreos.ignition+json; version=2.1.0, application/vnd.coreos.ignition+json; version=1; q=0.5, */*; q=0.1"}, + "Accept": []string{"application/vnd.coreos.ignition+json; version=2.2.0, application/vnd.coreos.ignition+json; version=1; q=0.5, */*; q=0.1"}, } ) diff --git a/tests/negative/files/append.go b/tests/negative/files/append.go index 759764db8..f6cdfd5ca 100644 --- a/tests/negative/files/append.go +++ b/tests/negative/files/append.go @@ -36,7 +36,7 @@ func AppendToDirectory() types.Test { } config := `{ - "ignition": {"version": "2.2.0-experimental"}, + "ignition": {"version": "2.2.0"}, "storage": { "files": [{ "filesystem": "root", @@ -76,7 +76,7 @@ func AppendAndOverwrite() types.Test { } config := `{ - "ignition": {"version": "2.2.0-experimental"}, + "ignition": {"version": "2.2.0"}, "storage": { "files": [{ "filesystem": "root", diff --git a/tests/negative/files/preexisting_nodes.go b/tests/negative/files/preexisting_nodes.go index 731e24a1b..7538a7269 100644 --- a/tests/negative/files/preexisting_nodes.go +++ b/tests/negative/files/preexisting_nodes.go @@ -33,7 +33,7 @@ func ForceFileCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -67,7 +67,7 @@ func ForceDirCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "directories": [{ "filesystem": "root", @@ -98,7 +98,7 @@ func ForceLinkCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -137,7 +137,7 @@ func ForceHardLinkCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -177,7 +177,7 @@ func ForceFileCreationOverNonemptyDir() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -212,7 +212,7 @@ func ForceLinkCreationOverNonemptyDir() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", diff --git a/tests/negative/general/config.go b/tests/negative/general/config.go index e194d83a3..1693405f9 100644 --- a/tests/negative/general/config.go +++ b/tests/negative/general/config.go @@ -28,6 +28,9 @@ func init() { register.Register(register.NegativeTest, AppendConfigWithMissingFileHTTP()) register.Register(register.NegativeTest, AppendConfigWithMissingFileTFTP()) register.Register(register.NegativeTest, AppendConfigWithMissingFileOEM()) + register.Register(register.NegativeTest, VersionOnlyConfig22()) + register.Register(register.NegativeTest, VersionOnlyConfig23()) + register.Register(register.NegativeTest, VersionOnlyConfig24()) } func ReplaceConfigWithInvalidHash() types.Test { @@ -236,3 +239,60 @@ func AppendConfigWithMissingFileOEM() types.Test { Config: config, } } + +func VersionOnlyConfig22() types.Test { + name := "Version Only Config 2.2.0-experimental" + in := types.GetBaseDisk() + out := in + config := `{ + "ignition": { + "version": "2.2.0-experimental" + } + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigShouldBeBad: true, + } +} + +func VersionOnlyConfig23() types.Test { + name := "Version Only Config 2.3.0" + in := types.GetBaseDisk() + out := in + config := `{ + "ignition": { + "version": "2.3.0" + } + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigShouldBeBad: true, + } +} + +func VersionOnlyConfig24() types.Test { + name := "Version Only Config 2.4.0-experimental" + in := types.GetBaseDisk() + out := in + config := `{ + "ignition": { + "version": "2.4.0-experimental" + } + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigShouldBeBad: true, + } +} diff --git a/tests/negative/networkd/dropins.go b/tests/negative/networkd/dropins.go index cb9d66790..59034c165 100644 --- a/tests/negative/networkd/dropins.go +++ b/tests/negative/networkd/dropins.go @@ -28,7 +28,7 @@ func NetworkdDropinInvalidExtension() types.Test { in := types.GetBaseDisk() out := in config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "networkd": { "units": [{ "name": "static.network", diff --git a/tests/negative/security/tls.go b/tests/negative/security/tls.go index fdc674138..15d1395c4 100644 --- a/tests/negative/security/tls.go +++ b/tests/negative/security/tls.go @@ -96,7 +96,7 @@ func AppendConfigCustomCert() types.Test { out := types.GetBaseDisk() config := fmt.Sprintf(`{ "ignition": { - "version": "2.2.0-experimental", + "version": "2.2.0", "config": { "append": [{ "source": %q @@ -122,7 +122,7 @@ func FetchFileCustomCert() types.Test { out := types.GetBaseDisk() config := fmt.Sprintf(`{ "ignition": { - "version": "2.2.0-experimental", + "version": "2.2.0", "timeouts": { "httpTotal": 5 } diff --git a/tests/positive/files/directory.go b/tests/positive/files/directory.go index 015dddb17..b7db460f0 100644 --- a/tests/positive/files/directory.go +++ b/tests/positive/files/directory.go @@ -60,7 +60,7 @@ func ForceDirCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "directories": [{ "filesystem": "root", @@ -100,7 +100,7 @@ func ForceDirCreationOverNonemptyDir() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "directories": [{ "filesystem": "root", diff --git a/tests/positive/files/file.go b/tests/positive/files/file.go index 09f813d98..43557f2e8 100644 --- a/tests/positive/files/file.go +++ b/tests/positive/files/file.go @@ -176,7 +176,7 @@ func ForceFileCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -220,7 +220,7 @@ func ForceFileCreationNoOverwrite() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -263,7 +263,7 @@ func AppendToAFile() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -305,7 +305,7 @@ func AppendToNonexistentFile() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", diff --git a/tests/positive/files/link.go b/tests/positive/files/link.go index 1ba915930..e6e0726e9 100644 --- a/tests/positive/files/link.go +++ b/tests/positive/files/link.go @@ -123,7 +123,7 @@ func ForceLinkCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", @@ -181,7 +181,7 @@ func ForceHardLinkCreation() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "storage": { "files": [{ "filesystem": "root", diff --git a/tests/positive/general/general.go b/tests/positive/general/general.go index 6a3412cd6..bc8425903 100644 --- a/tests/positive/general/general.go +++ b/tests/positive/general/general.go @@ -30,7 +30,10 @@ func init() { register.Register(register.PositiveTest, AppendConfigWithRemoteConfigOEM()) register.Register(register.PositiveTest, ReplaceConfigWithRemoteConfigData()) register.Register(register.PositiveTest, AppendConfigWithRemoteConfigData()) - register.Register(register.PositiveTest, VersionOnlyConfig()) + register.Register(register.PositiveTest, VersionOnlyConfig20()) + register.Register(register.PositiveTest, VersionOnlyConfig21()) + register.Register(register.PositiveTest, VersionOnlyConfig22()) + register.Register(register.PositiveTest, VersionOnlyConfig23()) register.Register(register.PositiveTest, EmptyUserdata()) } @@ -420,8 +423,24 @@ func AppendConfigWithRemoteConfigData() types.Test { } } -func VersionOnlyConfig() types.Test { - name := "Version Only Config" +func VersionOnlyConfig20() types.Test { + name := "Version Only Config 2.0.0" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + config := `{ + "ignition": {"version": "2.0.0"} + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + } +} + +func VersionOnlyConfig21() types.Test { + name := "Version Only Config 2.1.0" in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ @@ -436,6 +455,38 @@ func VersionOnlyConfig() types.Test { } } +func VersionOnlyConfig22() types.Test { + name := "Version Only Config 2.2.0" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + config := `{ + "ignition": {"version": "2.2.0"} + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + } +} + +func VersionOnlyConfig23() types.Test { + name := "Version Only Config 2.3.0-experimental" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + config := `{ + "ignition": {"version": "2.3.0-experimental"} + }` + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + } +} + func EmptyUserdata() types.Test { name := "Empty Userdata" in := types.GetBaseDisk() diff --git a/tests/positive/networkd/dropins.go b/tests/positive/networkd/dropins.go index 90aa41951..0afd8350f 100644 --- a/tests/positive/networkd/dropins.go +++ b/tests/positive/networkd/dropins.go @@ -28,7 +28,7 @@ func CreateNetworkdDropin() types.Test { in := types.GetBaseDisk() out := types.GetBaseDisk() config := `{ - "ignition": { "version": "2.2.0-experimental" }, + "ignition": { "version": "2.2.0" }, "networkd": { "units": [{ "name": "static.network", diff --git a/tests/positive/security/tls.go b/tests/positive/security/tls.go index dde9a5e39..5528476e0 100644 --- a/tests/positive/security/tls.go +++ b/tests/positive/security/tls.go @@ -95,7 +95,7 @@ func AppendConfigCustomCert() types.Test { out := types.GetBaseDisk() config := fmt.Sprintf(`{ "ignition": { - "version": "2.2.0-experimental", + "version": "2.2.0", "config": { "append": [{ "source": %q @@ -135,7 +135,7 @@ func FetchFileCustomCert() types.Test { out := types.GetBaseDisk() config := fmt.Sprintf(`{ "ignition": { - "version": "2.2.0-experimental", + "version": "2.2.0", "security": { "tls": { "certificateAuthorities": [{