diff --git a/config/config.go b/config/config.go index e4a2172d8..47e822146 100644 --- a/config/config.go +++ b/config/config.go @@ -15,8 +15,8 @@ package config import ( - exp "github.com/coreos/ignition/v2/config/v3_5_experimental" - types_exp "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + exp "github.com/coreos/ignition/v2/config/v3_6_experimental" + types_exp "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/vcontext/report" ) diff --git a/config/config_test.go b/config/config_test.go index 1e35aee98..a38159deb 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -25,7 +25,8 @@ import ( v3_2 "github.com/coreos/ignition/v2/config/v3_2/types" v3_3 "github.com/coreos/ignition/v2/config/v3_3/types" v3_4 "github.com/coreos/ignition/v2/config/v3_4/types" - v3_5 "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + v3_5 "github.com/coreos/ignition/v2/config/v3_5/types" + v3_6 "github.com/coreos/ignition/v2/config/v3_6_experimental/types" ) type typeSet map[reflect.Type]struct{} @@ -274,6 +275,7 @@ func TestConfigStructure(t *testing.T) { reflect.TypeOf(v3_3.Config{}), reflect.TypeOf(v3_4.Config{}), reflect.TypeOf(v3_5.Config{}), + reflect.TypeOf(v3_6.Config{}), } for _, configType := range configs { diff --git a/config/merge/merge_test.go b/config/merge/merge_test.go index e1dbd9366..ab1221717 100644 --- a/config/merge/merge_test.go +++ b/config/merge/merge_test.go @@ -19,7 +19,7 @@ import ( "github.com/coreos/ignition/v2/config/util" v3_2 "github.com/coreos/ignition/v2/config/v3_2/types" - "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/vcontext/path" "github.com/stretchr/testify/assert" diff --git a/config/v3_5/config.go b/config/v3_5/config.go new file mode 100644 index 000000000..d6f8645bb --- /dev/null +++ b/config/v3_5/config.go @@ -0,0 +1,78 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3_5 + +import ( + "github.com/coreos/ignition/v2/config/merge" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + prev "github.com/coreos/ignition/v2/config/v3_4" + "github.com/coreos/ignition/v2/config/v3_5/translate" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/ignition/v2/config/validate" + + "github.com/coreos/go-semver/semver" + "github.com/coreos/vcontext/report" +) + +func Merge(parent, child types.Config) types.Config { + res, _ := merge.MergeStructTranscribe(parent, child) + return res.(types.Config) +} + +// Parse parses the raw config into a types.Config struct and generates a report of any +// errors, warnings, info, and deprecations it encountered +func Parse(rawConfig []byte) (types.Config, report.Report, error) { + if len(rawConfig) == 0 { + return types.Config{}, report.Report{}, errors.ErrEmpty + } + + var config types.Config + if rpt, err := util.HandleParseErrors(rawConfig, &config); err != nil { + return types.Config{}, rpt, err + } + + version, err := semver.NewVersion(config.Ignition.Version) + + if err != nil || *version != types.MaxVersion { + return types.Config{}, report.Report{}, errors.ErrUnknownVersion + } + + rpt := validate.ValidateWithContext(config, rawConfig) + if rpt.IsFatal() { + return types.Config{}, rpt, errors.ErrInvalid + } + + return config, rpt, nil +} + +// ParseCompatibleVersion parses the raw config of version 3.5.0 or +// lesser into a 3.5 types.Config struct and generates a report of any errors, +// warnings, info, and deprecations it encountered +func ParseCompatibleVersion(raw []byte) (types.Config, report.Report, error) { + version, rpt, err := util.GetConfigVersion(raw) + if err != nil { + return types.Config{}, rpt, err + } + + if version == types.MaxVersion { + return Parse(raw) + } + prevCfg, r, err := prev.ParseCompatibleVersion(raw) + if err != nil { + return types.Config{}, r, err + } + return translate.Translate(prevCfg), r, nil +} diff --git a/config/v3_5_experimental/config_test.go b/config/v3_5/config_test.go similarity index 91% rename from config/v3_5_experimental/config_test.go rename to config/v3_5/config_test.go index a56b4b3a2..9786befc3 100644 --- a/config/v3_5_experimental/config_test.go +++ b/config/v3_5/config_test.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package v3_5_experimental +package v3_5 import ( "testing" "github.com/coreos/ignition/v2/config/shared/errors" - "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + "github.com/coreos/ignition/v2/config/v3_5/types" "github.com/stretchr/testify/assert" ) @@ -83,6 +83,10 @@ func TestParse(t *testing.T) { in: in{config: []byte(`{"ignition": {"version": "3.4.0"}}`)}, out: out{err: errors.ErrUnknownVersion}, }, + { + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, { in: in{config: []byte(`{"ignition": {"version": "2.0.0-experimental"}}`)}, out: out{err: errors.ErrUnknownVersion}, @@ -129,7 +133,7 @@ func TestParse(t *testing.T) { }, { in: in{config: []byte(`{"ignition": {"version": "3.5.0-experimental"}}`)}, - out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + out: out{err: errors.ErrUnknownVersion}, }, { in: in{config: []byte(`{"ignition": {"version": "2.0.0"},}`)}, @@ -148,7 +152,7 @@ func TestParse(t *testing.T) { out: out{err: errors.ErrEmpty}, }, { - in: in{config: []byte(`{"ignition": {"version": "3.5.0-experimental"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, out: out{err: errors.ErrInvalid}, }, } @@ -178,15 +182,15 @@ func TestParse(t *testing.T) { out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, }, { - in: in{config: []byte(`{"ignition": {"version": "3.5.0-experimental"}}`)}, + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}}`)}, out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, }, { - in: in{config: []byte(`{"ignition": {"version": "3.5.0"}}`)}, + in: in{config: []byte(`{"ignition": {"version": "3.6.0-experimental"}}`)}, out: out{err: errors.ErrUnknownVersion}, }, { - in: in{config: []byte(`{"ignition": {"version": "3.6.0"}}`)}, + in: in{config: []byte(`{"ignition": {"version": "3.7.0"}}`)}, out: out{err: errors.ErrUnknownVersion}, }, { @@ -198,7 +202,7 @@ func TestParse(t *testing.T) { out: out{err: errors.ErrInvalid}, }, { - in: in{config: []byte(`{"ignition": {"version": "3.5.0-experimental"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, out: out{err: errors.ErrInvalid}, }, } diff --git a/config/v3_5_experimental/schema/ignition.json b/config/v3_5/schema/ignition.json similarity index 100% rename from config/v3_5_experimental/schema/ignition.json rename to config/v3_5/schema/ignition.json diff --git a/config/v3_5_experimental/translate/translate.go b/config/v3_5/translate/translate.go similarity index 96% rename from config/v3_5_experimental/translate/translate.go rename to config/v3_5/translate/translate.go index 19f6da5d1..bc13973fc 100644 --- a/config/v3_5_experimental/translate/translate.go +++ b/config/v3_5/translate/translate.go @@ -17,7 +17,7 @@ package translate import ( "github.com/coreos/ignition/v2/config/translate" old_types "github.com/coreos/ignition/v2/config/v3_4/types" - "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + "github.com/coreos/ignition/v2/config/v3_5/types" ) func translateIgnition(old old_types.Ignition) (ret types.Ignition) { diff --git a/config/v3_5_experimental/translate/translate_test.go b/config/v3_5/translate/translate_test.go similarity index 100% rename from config/v3_5_experimental/translate/translate_test.go rename to config/v3_5/translate/translate_test.go diff --git a/config/v3_5_experimental/types/cex.go b/config/v3_5/types/cex.go similarity index 100% rename from config/v3_5_experimental/types/cex.go rename to config/v3_5/types/cex.go diff --git a/config/v3_5_experimental/types/cex_test.go b/config/v3_5/types/cex_test.go similarity index 100% rename from config/v3_5_experimental/types/cex_test.go rename to config/v3_5/types/cex_test.go diff --git a/config/v3_5_experimental/types/clevis.go b/config/v3_5/types/clevis.go similarity index 100% rename from config/v3_5_experimental/types/clevis.go rename to config/v3_5/types/clevis.go diff --git a/config/v3_5_experimental/types/clevis_test.go b/config/v3_5/types/clevis_test.go similarity index 100% rename from config/v3_5_experimental/types/clevis_test.go rename to config/v3_5/types/clevis_test.go diff --git a/config/v3_5/types/config.go b/config/v3_5/types/config.go new file mode 100644 index 000000000..659657b63 --- /dev/null +++ b/config/v3_5/types/config.go @@ -0,0 +1,64 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/go-semver/semver" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + MaxVersion = semver.Version{ + Major: 3, + Minor: 5, + } +) + +func (cfg Config) Validate(c path.ContextPath) (r report.Report) { + systemdPath := "/etc/systemd/system/" + unitPaths := map[string]struct{}{} + for _, unit := range cfg.Systemd.Units { + if !util.NilOrEmpty(unit.Contents) { + pathString := systemdPath + unit.Name + unitPaths[pathString] = struct{}{} + } + for _, dropin := range unit.Dropins { + if !util.NilOrEmpty(dropin.Contents) { + pathString := systemdPath + unit.Name + ".d/" + dropin.Name + unitPaths[pathString] = struct{}{} + } + } + } + for i, f := range cfg.Storage.Files { + if _, exists := unitPaths[f.Path]; exists { + r.AddOnError(c.Append("storage", "files", i, "path"), errors.ErrPathConflictsSystemd) + } + } + for i, d := range cfg.Storage.Directories { + if _, exists := unitPaths[d.Path]; exists { + r.AddOnError(c.Append("storage", "directories", i, "path"), errors.ErrPathConflictsSystemd) + } + } + for i, l := range cfg.Storage.Links { + if _, exists := unitPaths[l.Path]; exists { + r.AddOnError(c.Append("storage", "links", i, "path"), errors.ErrPathConflictsSystemd) + } + } + return +} diff --git a/config/v3_5_experimental/types/config_test.go b/config/v3_5/types/config_test.go similarity index 100% rename from config/v3_5_experimental/types/config_test.go rename to config/v3_5/types/config_test.go diff --git a/config/v3_5_experimental/types/device.go b/config/v3_5/types/device.go similarity index 100% rename from config/v3_5_experimental/types/device.go rename to config/v3_5/types/device.go diff --git a/config/v3_5_experimental/types/directory.go b/config/v3_5/types/directory.go similarity index 100% rename from config/v3_5_experimental/types/directory.go rename to config/v3_5/types/directory.go diff --git a/config/v3_5_experimental/types/disk.go b/config/v3_5/types/disk.go similarity index 100% rename from config/v3_5_experimental/types/disk.go rename to config/v3_5/types/disk.go diff --git a/config/v3_5_experimental/types/file.go b/config/v3_5/types/file.go similarity index 100% rename from config/v3_5_experimental/types/file.go rename to config/v3_5/types/file.go diff --git a/config/v3_5_experimental/types/file_test.go b/config/v3_5/types/file_test.go similarity index 100% rename from config/v3_5_experimental/types/file_test.go rename to config/v3_5/types/file_test.go diff --git a/config/v3_5_experimental/types/filesystem.go b/config/v3_5/types/filesystem.go similarity index 100% rename from config/v3_5_experimental/types/filesystem.go rename to config/v3_5/types/filesystem.go diff --git a/config/v3_5_experimental/types/filesystem_test.go b/config/v3_5/types/filesystem_test.go similarity index 100% rename from config/v3_5_experimental/types/filesystem_test.go rename to config/v3_5/types/filesystem_test.go diff --git a/config/v3_5_experimental/types/headers.go b/config/v3_5/types/headers.go similarity index 100% rename from config/v3_5_experimental/types/headers.go rename to config/v3_5/types/headers.go diff --git a/config/v3_5_experimental/types/headers_test.go b/config/v3_5/types/headers_test.go similarity index 100% rename from config/v3_5_experimental/types/headers_test.go rename to config/v3_5/types/headers_test.go diff --git a/config/v3_5_experimental/types/ignition.go b/config/v3_5/types/ignition.go similarity index 100% rename from config/v3_5_experimental/types/ignition.go rename to config/v3_5/types/ignition.go diff --git a/config/v3_5_experimental/types/ignition_test.go b/config/v3_5/types/ignition_test.go similarity index 100% rename from config/v3_5_experimental/types/ignition_test.go rename to config/v3_5/types/ignition_test.go diff --git a/config/v3_5_experimental/types/kargs.go b/config/v3_5/types/kargs.go similarity index 100% rename from config/v3_5_experimental/types/kargs.go rename to config/v3_5/types/kargs.go diff --git a/config/v3_5_experimental/types/kargs_test.go b/config/v3_5/types/kargs_test.go similarity index 100% rename from config/v3_5_experimental/types/kargs_test.go rename to config/v3_5/types/kargs_test.go diff --git a/config/v3_5_experimental/types/luks.go b/config/v3_5/types/luks.go similarity index 100% rename from config/v3_5_experimental/types/luks.go rename to config/v3_5/types/luks.go diff --git a/config/v3_5_experimental/types/mode.go b/config/v3_5/types/mode.go similarity index 100% rename from config/v3_5_experimental/types/mode.go rename to config/v3_5/types/mode.go diff --git a/config/v3_5_experimental/types/mode_test.go b/config/v3_5/types/mode_test.go similarity index 100% rename from config/v3_5_experimental/types/mode_test.go rename to config/v3_5/types/mode_test.go diff --git a/config/v3_5_experimental/types/node.go b/config/v3_5/types/node.go similarity index 100% rename from config/v3_5_experimental/types/node.go rename to config/v3_5/types/node.go diff --git a/config/v3_5_experimental/types/node_test.go b/config/v3_5/types/node_test.go similarity index 100% rename from config/v3_5_experimental/types/node_test.go rename to config/v3_5/types/node_test.go diff --git a/config/v3_5_experimental/types/partition.go b/config/v3_5/types/partition.go similarity index 100% rename from config/v3_5_experimental/types/partition.go rename to config/v3_5/types/partition.go diff --git a/config/v3_5_experimental/types/partition_test.go b/config/v3_5/types/partition_test.go similarity index 100% rename from config/v3_5_experimental/types/partition_test.go rename to config/v3_5/types/partition_test.go diff --git a/config/v3_5_experimental/types/passwd.go b/config/v3_5/types/passwd.go similarity index 100% rename from config/v3_5_experimental/types/passwd.go rename to config/v3_5/types/passwd.go diff --git a/config/v3_5_experimental/types/path.go b/config/v3_5/types/path.go similarity index 100% rename from config/v3_5_experimental/types/path.go rename to config/v3_5/types/path.go diff --git a/config/v3_5_experimental/types/path_test.go b/config/v3_5/types/path_test.go similarity index 100% rename from config/v3_5_experimental/types/path_test.go rename to config/v3_5/types/path_test.go diff --git a/config/v3_5_experimental/types/proxy.go b/config/v3_5/types/proxy.go similarity index 100% rename from config/v3_5_experimental/types/proxy.go rename to config/v3_5/types/proxy.go diff --git a/config/v3_5_experimental/types/proxy_test.go b/config/v3_5/types/proxy_test.go similarity index 100% rename from config/v3_5_experimental/types/proxy_test.go rename to config/v3_5/types/proxy_test.go diff --git a/config/v3_5_experimental/types/raid.go b/config/v3_5/types/raid.go similarity index 100% rename from config/v3_5_experimental/types/raid.go rename to config/v3_5/types/raid.go diff --git a/config/v3_5_experimental/types/raid_test.go b/config/v3_5/types/raid_test.go similarity index 100% rename from config/v3_5_experimental/types/raid_test.go rename to config/v3_5/types/raid_test.go diff --git a/config/v3_5_experimental/types/resource.go b/config/v3_5/types/resource.go similarity index 100% rename from config/v3_5_experimental/types/resource.go rename to config/v3_5/types/resource.go diff --git a/config/v3_5/types/schema.go b/config/v3_5/types/schema.go new file mode 100644 index 000000000..6c8c42f57 --- /dev/null +++ b/config/v3_5/types/schema.go @@ -0,0 +1,264 @@ +package types + +// generated by "schematyper --package=types config/v3_5/schema/ignition.json -o config/v3_5/types/schema.go --root-type=Config" -- DO NOT EDIT + +type Cex struct { + Enabled *bool `json:"enabled,omitempty"` +} + +type Clevis struct { + Custom ClevisCustom `json:"custom,omitempty"` + Tang []Tang `json:"tang,omitempty"` + Threshold *int `json:"threshold,omitempty"` + Tpm2 *bool `json:"tpm2,omitempty"` +} + +type ClevisCustom struct { + Config *string `json:"config,omitempty"` + NeedsNetwork *bool `json:"needsNetwork,omitempty"` + Pin *string `json:"pin,omitempty"` +} + +type Config struct { + Ignition Ignition `json:"ignition"` + KernelArguments KernelArguments `json:"kernelArguments,omitempty"` + Passwd Passwd `json:"passwd,omitempty"` + Storage Storage `json:"storage,omitempty"` + Systemd Systemd `json:"systemd,omitempty"` +} + +type Device string + +type Directory struct { + Node + DirectoryEmbedded1 +} + +type DirectoryEmbedded1 struct { + Mode *int `json:"mode,omitempty"` +} + +type Disk struct { + Device string `json:"device"` + Partitions []Partition `json:"partitions,omitempty"` + WipeTable *bool `json:"wipeTable,omitempty"` +} + +type Dropin struct { + Contents *string `json:"contents,omitempty"` + Name string `json:"name"` +} + +type File struct { + Node + FileEmbedded1 +} + +type FileEmbedded1 struct { + Append []Resource `json:"append,omitempty"` + Contents Resource `json:"contents,omitempty"` + Mode *int `json:"mode,omitempty"` +} + +type Filesystem struct { + Device string `json:"device"` + Format *string `json:"format,omitempty"` + Label *string `json:"label,omitempty"` + MountOptions []MountOption `json:"mountOptions,omitempty"` + Options []FilesystemOption `json:"options,omitempty"` + Path *string `json:"path,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeFilesystem *bool `json:"wipeFilesystem,omitempty"` +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `json:"name"` + Value *string `json:"value,omitempty"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `json:"config,omitempty"` + Proxy Proxy `json:"proxy,omitempty"` + Security Security `json:"security,omitempty"` + Timeouts Timeouts `json:"timeouts,omitempty"` + Version string `json:"version"` +} + +type IgnitionConfig struct { + Merge []Resource `json:"merge,omitempty"` + Replace Resource `json:"replace,omitempty"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `json:"shouldExist,omitempty"` + ShouldNotExist []KernelArgument `json:"shouldNotExist,omitempty"` +} + +type Link struct { + Node + LinkEmbedded1 +} + +type LinkEmbedded1 struct { + Hard *bool `json:"hard,omitempty"` + Target *string `json:"target,omitempty"` +} + +type Luks struct { + Cex Cex `json:"cex,omitempty"` + Clevis Clevis `json:"clevis,omitempty"` + Device *string `json:"device,omitempty"` + Discard *bool `json:"discard,omitempty"` + KeyFile Resource `json:"keyFile,omitempty"` + Label *string `json:"label,omitempty"` + Name string `json:"name"` + OpenOptions []OpenOption `json:"openOptions,omitempty"` + Options []LuksOption `json:"options,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeVolume *bool `json:"wipeVolume,omitempty"` +} + +type LuksOption string + +type MountOption string + +type NoProxyItem string + +type Node struct { + Group NodeGroup `json:"group,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` + Path string `json:"path"` + 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 OpenOption string + +type Partition struct { + GUID *string `json:"guid,omitempty"` + Label *string `json:"label,omitempty"` + Number int `json:"number,omitempty"` + Resize *bool `json:"resize,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + SizeMiB *int `json:"sizeMiB,omitempty"` + StartMiB *int `json:"startMiB,omitempty"` + TypeGUID *string `json:"typeGuid,omitempty"` + WipePartitionEntry *bool `json:"wipePartitionEntry,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"` + PasswordHash *string `json:"passwordHash,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` +} + +type PasswdUser struct { + Gecos *string `json:"gecos,omitempty"` + Groups []Group `json:"groups,omitempty"` + HomeDir *string `json:"homeDir,omitempty"` + Name string `json:"name"` + 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"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` + UID *int `json:"uid,omitempty"` +} + +type Proxy struct { + HTTPProxy *string `json:"httpProxy,omitempty"` + HTTPSProxy *string `json:"httpsProxy,omitempty"` + NoProxy []NoProxyItem `json:"noProxy,omitempty"` +} + +type Raid struct { + Devices []Device `json:"devices,omitempty"` + Level *string `json:"level,omitempty"` + Name string `json:"name"` + Options []RaidOption `json:"options,omitempty"` + Spares *int `json:"spares,omitempty"` +} + +type RaidOption string + +type Resource struct { + Compression *string `json:"compression,omitempty"` + HTTPHeaders HTTPHeaders `json:"httpHeaders,omitempty"` + Source *string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +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"` + Luks []Luks `json:"luks,omitempty"` + Raid []Raid `json:"raid,omitempty"` +} + +type Systemd struct { + Units []Unit `json:"units,omitempty"` +} + +type TLS struct { + CertificateAuthorities []Resource `json:"certificateAuthorities,omitempty"` +} + +type Tang struct { + Advertisement *string `json:"advertisement,omitempty"` + Thumbprint *string `json:"thumbprint,omitempty"` + URL string `json:"url,omitempty"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"` + HTTPTotal *int `json:"httpTotal,omitempty"` +} + +type Unit struct { + Contents *string `json:"contents,omitempty"` + Dropins []Dropin `json:"dropins,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Mask *bool `json:"mask,omitempty"` + Name string `json:"name"` +} + +type Verification struct { + Hash *string `json:"hash,omitempty"` +} diff --git a/config/v3_5_experimental/types/storage.go b/config/v3_5/types/storage.go similarity index 100% rename from config/v3_5_experimental/types/storage.go rename to config/v3_5/types/storage.go diff --git a/config/v3_5_experimental/types/storage_test.go b/config/v3_5/types/storage_test.go similarity index 100% rename from config/v3_5_experimental/types/storage_test.go rename to config/v3_5/types/storage_test.go diff --git a/config/v3_5_experimental/types/systemd.go b/config/v3_5/types/systemd.go similarity index 100% rename from config/v3_5_experimental/types/systemd.go rename to config/v3_5/types/systemd.go diff --git a/config/v3_5_experimental/types/systemd_test.go b/config/v3_5/types/systemd_test.go similarity index 100% rename from config/v3_5_experimental/types/systemd_test.go rename to config/v3_5/types/systemd_test.go diff --git a/config/v3_5_experimental/types/tang.go b/config/v3_5/types/tang.go similarity index 100% rename from config/v3_5_experimental/types/tang.go rename to config/v3_5/types/tang.go diff --git a/config/v3_5_experimental/types/tang_test.go b/config/v3_5/types/tang_test.go similarity index 100% rename from config/v3_5_experimental/types/tang_test.go rename to config/v3_5/types/tang_test.go diff --git a/config/v3_5_experimental/types/tls.go b/config/v3_5/types/tls.go similarity index 100% rename from config/v3_5_experimental/types/tls.go rename to config/v3_5/types/tls.go diff --git a/config/v3_5_experimental/types/tls_test.go b/config/v3_5/types/tls_test.go similarity index 100% rename from config/v3_5_experimental/types/tls_test.go rename to config/v3_5/types/tls_test.go diff --git a/config/v3_5_experimental/types/unit.go b/config/v3_5/types/unit.go similarity index 100% rename from config/v3_5_experimental/types/unit.go rename to config/v3_5/types/unit.go diff --git a/config/v3_5_experimental/types/unit_test.go b/config/v3_5/types/unit_test.go similarity index 100% rename from config/v3_5_experimental/types/unit_test.go rename to config/v3_5/types/unit_test.go diff --git a/config/v3_5_experimental/types/url.go b/config/v3_5/types/url.go similarity index 100% rename from config/v3_5_experimental/types/url.go rename to config/v3_5/types/url.go diff --git a/config/v3_5_experimental/types/url_test.go b/config/v3_5/types/url_test.go similarity index 100% rename from config/v3_5_experimental/types/url_test.go rename to config/v3_5/types/url_test.go diff --git a/config/v3_5_experimental/types/verification.go b/config/v3_5/types/verification.go similarity index 100% rename from config/v3_5_experimental/types/verification.go rename to config/v3_5/types/verification.go diff --git a/config/v3_5_experimental/types/verification_test.go b/config/v3_5/types/verification_test.go similarity index 100% rename from config/v3_5_experimental/types/verification_test.go rename to config/v3_5/types/verification_test.go diff --git a/config/v3_5_experimental/config.go b/config/v3_6_experimental/config.go similarity index 92% rename from config/v3_5_experimental/config.go rename to config/v3_6_experimental/config.go index 9a92bf2ac..e67b08bab 100644 --- a/config/v3_5_experimental/config.go +++ b/config/v3_6_experimental/config.go @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package v3_5_experimental +package v3_6_experimental import ( "github.com/coreos/ignition/v2/config/merge" "github.com/coreos/ignition/v2/config/shared/errors" "github.com/coreos/ignition/v2/config/util" - prev "github.com/coreos/ignition/v2/config/v3_4" - "github.com/coreos/ignition/v2/config/v3_5_experimental/translate" - "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + prev "github.com/coreos/ignition/v2/config/v3_5" + "github.com/coreos/ignition/v2/config/v3_6_experimental/translate" + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/config/validate" "github.com/coreos/go-semver/semver" diff --git a/config/v3_6_experimental/config_test.go b/config/v3_6_experimental/config_test.go new file mode 100644 index 000000000..8ba561631 --- /dev/null +++ b/config/v3_6_experimental/config_test.go @@ -0,0 +1,238 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v3_6_experimental + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + type in struct { + config []byte + } + type out struct { + config types.Config + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{config: []byte(`{"ignitionVersion": 1}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "1.0.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.0.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.1.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.2.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.3.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.4.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.0.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.1.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.2.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.3.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.4.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.0.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.1.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.2.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.3.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.4.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.5.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.0.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.1.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.2.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.3.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.4.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.5.0-experimental"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.6.0-experimental"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "2.0.0"},}`)}, + out: out{err: errors.ErrInvalid}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "invalid.semver"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte{}}, + out: out{err: errors.ErrEmpty}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.6.0-experimental"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, + out: out{err: errors.ErrInvalid}, + }, + } + + testsCompt := []struct { + in in + out out + }{ + { + in: in{config: []byte(`{"ignition": {"version": "3.0.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.1.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.2.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.3.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.4.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.5.0"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.6.0-experimental"}}`)}, + out: out{config: types.Config{Ignition: types.Ignition{Version: types.MaxVersion.String()}}}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.6.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.7.0"}}`)}, + out: out{err: errors.ErrUnknownVersion}, + }, + { + in: in{config: []byte{}}, + out: out{err: errors.ErrEmpty}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.0.0"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, + out: out{err: errors.ErrInvalid}, + }, + { + in: in{config: []byte(`{"ignition": {"version": "3.6.0-experimental"}, "storage": {"filesystems": [{"format": "ext4", "label": "zzzzzzzzzzzzzzzzzzzzzzzzzzz"}]}}`)}, + out: out{err: errors.ErrInvalid}, + }, + } + + for i, test := range tests { + config, report, err := Parse(test.in.config) + if test.out.err != err { + t.Errorf("#%d: bad error: want %v, got %v, report: %+v", i, test.out.err, err, report) + } + if test.out.err == errors.ErrInvalid && len(report.Entries) == 0 { + t.Errorf("#%d: expected report, got none", i) + } + assert.Equal(t, test.out.config, config, "#%d: bad config, report: %+v", i, report) + } + for i, test := range testsCompt { + config, report, err := ParseCompatibleVersion(test.in.config) + if test.out.err != err { + t.Errorf("#%d: bad error: want %v, got %v, report: %+v", i, test.out.err, err, report) + } + if test.out.err == errors.ErrInvalid && len(report.Entries) == 0 { + t.Errorf("#%d: expected report, got none", i) + } + assert.Equal(t, test.out.config, config, "#%d: bad config, report: %+v", i, report) + } +} diff --git a/config/v3_6_experimental/schema/ignition.json b/config/v3_6_experimental/schema/ignition.json new file mode 100644 index 000000000..344d06749 --- /dev/null +++ b/config/v3_6_experimental/schema/ignition.json @@ -0,0 +1,685 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ignition", + "type": "object", + "properties": { + "ignition": { + "$ref": "#/definitions/ignition" + }, + "storage": { + "$ref": "#/definitions/storage" + }, + "systemd": { + "$ref": "#/definitions/systemd" + }, + "passwd": { + "$ref": "#/definitions/passwd" + }, + "kernelArguments": { + "$ref": "#/definitions/kernelArguments" + } + }, + "required": [ + "ignition" + ], + "definitions": { + "resource": { + "type": "object", + "properties": { + "source": { + "type": ["string", "null"] + }, + "compression": { + "type": ["string", "null"] + }, + "httpHeaders": { + "$ref": "#/definitions/httpHeaders" + }, + "verification": { + "$ref": "#/definitions/verification" + } + } + }, + "verification": { + "type": "object", + "properties": { + "hash": { "type": ["string", "null"] } + } + }, + "httpHeaders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": ["string", "null"] + } + }, + "required": [ + "name" + ] + } + }, + "ignition": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/ignition/definitions/ignition-config" + }, + "timeouts": { + "$ref": "#/definitions/ignition/definitions/timeouts" + }, + "security": { + "$ref": "#/definitions/ignition/definitions/security" + }, + "proxy": { + "$ref": "#/definitions/ignition/definitions/proxy" + } + }, + "definitions": { + "ignition-config": { + "type": "object", + "properties": { + "merge": { + "type": "array", + "items": { + "$ref": "#/definitions/resource" + } + }, + "replace": { + "$ref": "#/definitions/resource" + } + } + }, + "security": { + "type": "object", + "properties": { + "tls": { + "type": "object", + "properties": { + "certificateAuthorities": { + "type": "array", + "items": { + "$ref": "#/definitions/resource" + } + } + } + } + } + }, + "proxy": { + "type": "object", + "properties": { + "httpProxy": { + "type": ["string", "null"] + }, + "httpsProxy": { + "type": ["string", "null"] + }, + "noProxy": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "timeouts": { + "type": "object", + "properties": { + "httpResponseHeaders": { + "type": ["integer", "null"] + }, + "httpTotal": { + "type": ["integer", "null"] + } + } + } + }, + "required": [ + "version" + ] + }, + "storage": { + "type": "object", + "properties": { + "disks": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/disk" + } + }, + "raid": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/raid" + } + }, + "luks": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/luks" + } + }, + "filesystems": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/filesystem" + } + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/file" + } + }, + "directories": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/directory" + } + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/link" + } + } + }, + "definitions": { + "disk": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "wipeTable": { + "type": ["boolean", "null"] + }, + "partitions": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/partition" + } + } + }, + "required": [ + "device" + ] + }, + "raid": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "type": ["string", "null"] + }, + "spares": { + "type": ["integer", "null"] + }, + "devices": { + "type": "array", + "items": { + "type": "string" + } + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name" + ] + }, + "luks": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": ["string", "null"] + }, + "uuid": { + "type": ["string", "null"] + }, + "device": { + "type": ["string", "null"] + }, + "keyFile": { + "$ref": "#/definitions/resource" + }, + "wipeVolume": { + "type": ["boolean", "null"] + }, + "clevis": { + "$ref": "#/definitions/storage/definitions/clevis" + }, + "cex": { + "$ref": "#/definitions/storage/definitions/cex" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "discard": { + "type": ["boolean", "null"] + }, + "openOptions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name" + ] + }, + "clevis": { + "type": "object", + "properties": { + "custom": { + "$ref": "#/definitions/storage/definitions/clevisCustom" + }, + "tpm2": { + "type": ["boolean", "null"] + }, + "tang": { + "type": "array", + "items": { + "$ref": "#/definitions/storage/definitions/tang" + } + }, + "threshold": { + "type": ["integer", "null"] + } + } + }, + "clevisCustom": { + "type": "object", + "properties": { + "pin": { + "type": ["string", "null"] + }, + "config": { + "type": ["string", "null"] + }, + "needsNetwork": { + "type": ["boolean", "null"] + } + } + }, + "tang": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "thumbprint": { + "type": ["string", "null"] + }, + "advertisement": { + "type": ["string", "null"] + } + } + }, + "cex": { + "type": "object", + "properties": { + "enabled": { + "type": ["boolean", "null"] + } + } + }, + "filesystem": { + "type": "object", + "properties": { + "path": { + "type": ["string", "null"] + }, + "device": { + "type": "string" + }, + "format": { + "type": ["string", "null"] + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "mountOptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "wipeFilesystem": { + "type": ["boolean", "null"] + }, + "label": { + "type": ["string", "null"] + }, + "uuid": { + "type": ["string", "null"] + } + }, + "required": [ + "device" + ] + }, + "file": { + "allOf": [ + { + "$ref": "#/definitions/storage/definitions/node" + }, + { + "type": "object", + "properties": { + "mode": { + "type": ["integer", "null"] + }, + "contents": { + "$ref": "#/definitions/resource" + }, + "append": { + "type": "array", + "items": { + "$ref": "#/definitions/resource" + } + } + } + } + ] + }, + "directory": { + "allOf": [ + { + "$ref": "#/definitions/storage/definitions/node" + }, + { + "type": "object", + "properties": { + "mode": { + "type": ["integer", "null"] + } + } + } + ] + }, + "link": { + "allOf": [ + { + "$ref": "#/definitions/storage/definitions/node" + }, + { + "type": "object", + "properties": { + "target": { + "type": ["string", "null"] + }, + "hard": { + "type": ["boolean", "null"] + } + } + } + ] + }, + "partition": { + "type": "object", + "properties": { + "label": { + "type": ["string", "null"] + }, + "number": { + "type": "integer" + }, + "sizeMiB": { + "type": ["integer", "null"] + }, + "startMiB": { + "type": ["integer", "null"] + }, + "typeGuid": { + "type": ["string", "null"] + }, + "guid": { + "type": ["string", "null"] + }, + "wipePartitionEntry": { + "type": ["boolean", "null"] + }, + "shouldExist": { + "type": ["boolean", "null"] + }, + "resize": { + "type": ["boolean", "null"] + } + } + }, + "node": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "overwrite": { + "type": ["boolean", "null"] + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": ["integer", "null"] + }, + "name": { + "type": ["string", "null"] + } + } + }, + "group": { + "type": "object", + "properties": { + "id": { + "type": ["integer", "null"] + }, + "name": { + "type": ["string", "null"] + } + } + } + }, + "required": [ + "path" + ] + } + } + }, + "systemd": { + "type": "object", + "properties": { + "units": { + "type": "array", + "items": { + "$ref": "#/definitions/systemd/definitions/unit" + } + } + }, + "definitions": { + "unit": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "enabled": { + "type": ["boolean", "null"] + }, + "mask": { + "type": ["boolean", "null"] + }, + "contents": { + "type": ["string", "null"] + }, + "dropins": { + "type": "array", + "items": { + "$ref": "#/definitions/systemd/definitions/dropin" + } + } + }, + "required": [ + "name" + ] + }, + "dropin": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "contents": { + "type": ["string", "null"] + } + }, + "required": [ + "name" + ] + } + } + }, + "kernelArguments": { + "type": "object", + "properties": { + "shouldExist": { + "type": "array", + "items": { + "$ref": "#/definitions/kernelArgument" + } + }, + "shouldNotExist": { + "type": "array", + "items": { + "$ref": "#/definitions/kernelArgument" + } + } + } + }, + "kernelArgument": { + "type": "string" + }, + "passwd": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/passwd/definitions/user" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/definitions/passwd/definitions/group" + } + } + }, + "definitions": { + "user": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "passwordHash": { + "type": ["string", "null"] + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "uid": { + "type": ["integer", "null"] + }, + "gecos": { + "type": ["string", "null"] + }, + "homeDir": { + "type": ["string", "null"] + }, + "noCreateHome": { + "type": ["boolean", "null"] + }, + "primaryGroup": { + "type": ["string", "null"] + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "noUserGroup": { + "type": ["boolean", "null"] + }, + "system": { + "type": ["boolean", "null"] + }, + "noLogInit": { + "type": ["boolean", "null"] + }, + "shell": { + "type": ["string", "null"] + }, + "shouldExist": { + "type": ["boolean", "null"] + } + }, + "required": [ + "name" + ] + }, + "group": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "gid": { + "type": ["integer", "null"] + }, + "passwordHash": { + "type": ["string", "null"] + }, + "system": { + "type": ["boolean", "null"] + }, + "shouldExist": { + "type": ["boolean", "null"] + } + }, + "required": [ + "name" + ] + } + } + } + } +} diff --git a/config/v3_6_experimental/translate/translate.go b/config/v3_6_experimental/translate/translate.go new file mode 100644 index 000000000..bf3f11e98 --- /dev/null +++ b/config/v3_6_experimental/translate/translate.go @@ -0,0 +1,35 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translate + +import ( + "github.com/coreos/ignition/v2/config/translate" + old_types "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" +) + +func translateIgnition(old old_types.Ignition) (ret types.Ignition) { + // use a new translator so we don't recurse infinitely + translate.NewTranslator().Translate(&old, &ret) + ret.Version = types.MaxVersion.String() + return +} + +func Translate(old old_types.Config) (ret types.Config) { + tr := translate.NewTranslator() + tr.AddCustomTranslator(translateIgnition) + tr.Translate(&old, &ret) + return +} diff --git a/config/v3_6_experimental/translate/translate_test.go b/config/v3_6_experimental/translate/translate_test.go new file mode 100644 index 000000000..fc9e3de55 --- /dev/null +++ b/config/v3_6_experimental/translate/translate_test.go @@ -0,0 +1,32 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translate + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/util" + old "github.com/coreos/ignition/v2/config/v3_5/types" +) + +// Check that we have valid translators for the complete config struct +// hierarchy; Translate will panic if not. We need to use a deeply non-zero +// struct to ensure translation descends into every type. +func TestTranslate(t *testing.T) { + typ := reflect.TypeOf(old.Config{}) + config := util.NonZeroValue(typ).Interface().(old.Config) + Translate(config) +} diff --git a/config/v3_6_experimental/types/cex.go b/config/v3_6_experimental/types/cex.go new file mode 100644 index 000000000..b34f5f527 --- /dev/null +++ b/config/v3_6_experimental/types/cex.go @@ -0,0 +1,33 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (cm Cex) IsPresent() bool { + return util.IsTrue(cm.Enabled) +} + +func (cx Cex) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(cx.Enabled) { + return + } + return +} diff --git a/config/v3_6_experimental/types/cex_test.go b/config/v3_6_experimental/types/cex_test.go new file mode 100644 index 000000000..35d562d8f --- /dev/null +++ b/config/v3_6_experimental/types/cex_test.go @@ -0,0 +1,53 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestCexValidate(t *testing.T) { + tests := []struct { + in Cex + at path.ContextPath + out error + }{ + { + in: Cex{}, + out: nil, + }, + { + in: Cex{ + Enabled: util.BoolToPtr(true), + }, + out: nil, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_6_experimental/types/clevis.go b/config/v3_6_experimental/types/clevis.go new file mode 100644 index 000000000..68887d434 --- /dev/null +++ b/config/v3_6_experimental/types/clevis.go @@ -0,0 +1,49 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (c Clevis) IsPresent() bool { + return util.NotEmpty(c.Custom.Pin) || + len(c.Tang) > 0 || + util.IsTrue(c.Tpm2) || + c.Threshold != nil && *c.Threshold != 0 +} + +func (cu ClevisCustom) Validate(c path.ContextPath) (r report.Report) { + if util.NilOrEmpty(cu.Pin) && util.NilOrEmpty(cu.Config) && !util.IsTrue(cu.NeedsNetwork) { + return + } + if util.NotEmpty(cu.Pin) { + switch *cu.Pin { + case "tpm2", "tang", "sss": + default: + r.AddOnError(c.Append("pin"), errors.ErrUnknownClevisPin) + } + } else { + r.AddOnError(c.Append("pin"), errors.ErrClevisPinRequired) + } + if util.NilOrEmpty(cu.Config) { + r.AddOnError(c.Append("config"), errors.ErrClevisConfigRequired) + } + return +} diff --git a/config/v3_6_experimental/types/clevis_test.go b/config/v3_6_experimental/types/clevis_test.go new file mode 100644 index 000000000..f616868cd --- /dev/null +++ b/config/v3_6_experimental/types/clevis_test.go @@ -0,0 +1,78 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestClevisCustomValidate(t *testing.T) { + tests := []struct { + in ClevisCustom + at path.ContextPath + out error + }{ + { + in: ClevisCustom{}, + out: nil, + }, + { + in: ClevisCustom{ + Config: util.StrToPtr("z"), + NeedsNetwork: util.BoolToPtr(true), + Pin: util.StrToPtr("sss"), + }, + out: nil, + }, + { + in: ClevisCustom{ + Config: util.StrToPtr("z"), + }, + at: path.New("", "pin"), + out: errors.ErrClevisPinRequired, + }, + { + in: ClevisCustom{ + Config: util.StrToPtr("z"), + Pin: util.StrToPtr("z"), + }, + at: path.New("", "pin"), + out: errors.ErrUnknownClevisPin, + }, + { + in: ClevisCustom{ + Pin: util.StrToPtr("tpm2"), + }, + at: path.New("", "config"), + out: errors.ErrClevisConfigRequired, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_5_experimental/types/config.go b/config/v3_6_experimental/types/config.go similarity index 99% rename from config/v3_5_experimental/types/config.go rename to config/v3_6_experimental/types/config.go index 40c27106e..9428b0bb2 100644 --- a/config/v3_5_experimental/types/config.go +++ b/config/v3_6_experimental/types/config.go @@ -26,7 +26,7 @@ import ( var ( MaxVersion = semver.Version{ Major: 3, - Minor: 5, + Minor: 6, PreRelease: "experimental", } ) diff --git a/config/v3_6_experimental/types/config_test.go b/config/v3_6_experimental/types/config_test.go new file mode 100644 index 000000000..3d82627b2 --- /dev/null +++ b/config/v3_6_experimental/types/config_test.go @@ -0,0 +1,260 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestConfigValidation(t *testing.T) { + tests := []struct { + in Config + out error + at path.ContextPath + }{ + // test 0: file conflicts with systemd dropin file, error + { + in: Config{ + Storage: Storage{ + Files: []File{ + { + Node: Node{Path: "/etc/systemd/system/foo.service.d/bar.conf"}, + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "foo.service", + Dropins: []Dropin{ + { + Name: "bar.conf", + Contents: util.StrToPtr("[Foo]\nQux=Bar"), + }, + }, + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "files", 0, "path"), + }, + // test 1: file conflicts with systemd unit, error + { + in: Config{ + Storage: Storage{ + Files: []File{ + { + Node: Node{Path: "/etc/systemd/system/foo.service"}, + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "foo.service", + Contents: util.StrToPtr("[Foo]\nQux=Bar"), + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "files", 0, "path"), + }, + // test 2: directory conflicts with systemd dropin file, error + { + in: Config{ + Storage: Storage{ + Directories: []Directory{ + { + Node: Node{Path: "/etc/systemd/system/foo.service.d/bar.conf"}, + }, + }, + }, + Systemd: Systemd{ + []Unit{ + { + Name: "foo.service", + Dropins: []Dropin{ + { + Name: "bar.conf", + Contents: util.StrToPtr("[Foo]\nQux=Bar"), + }, + }, + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "directories", 0, "path"), + }, + // test 3: directory conflicts with systemd unit, error + { + in: Config{ + Storage: Storage{ + Directories: []Directory{ + { + Node: Node{Path: "/etc/systemd/system/foo.service"}, + }, + }, + }, + Systemd: Systemd{ + []Unit{ + { + Name: "foo.service", + Contents: util.StrToPtr("[foo]\nQux=Baz"), + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "directories", 0, "path"), + }, + // test 4: link conflicts with systemd dropin file, error + { + in: Config{ + Storage: Storage{ + Links: []Link{ + { + Node: Node{Path: "/etc/systemd/system/foo.service.d/bar.conf"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/qux.conf")}, + }, + }, + }, + Systemd: Systemd{ + []Unit{ + { + Name: "foo.service", + Dropins: []Dropin{ + { + Name: "bar.conf", + Contents: util.StrToPtr("[Foo]\nQux=Bar"), + }, + }, + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "links", 0, "path"), + }, + // test 5: link conflicts with systemd unit, error + { + in: Config{ + Storage: Storage{ + Links: []Link{ + { + Node: Node{Path: "/etc/systemd/system/foo.service"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/qux.conf")}, + }, + }, + }, + Systemd: Systemd{ + []Unit{ + { + Name: "foo.service", + Contents: util.StrToPtr("[foo]\nQux=Baz"), + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + out: errors.ErrPathConflictsSystemd, + at: path.New("json", "storage", "links", 0, "path"), + }, + // test 6: non-conflicting scenarios + { + in: Config{ + Storage: Storage{ + Files: []File{ + { + Node: Node{Path: "/etc/systemd/system/bar.service.d/baz.conf"}, + }, + { + Node: Node{Path: "/etc/systemd/system/bar.service"}, + }, + { + Node: Node{Path: "/etc/systemd/system/foo.service.d/qux.conf"}, + }, + }, + Links: []Link{ + { + Node: Node{Path: "/etc/systemd/system/qux.service"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/qux.conf")}, + }, + { + Node: Node{Path: "/etc/systemd/system/quux.service.d/foo.conf"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo.conf")}, + }, + }, + Directories: []Directory{ + { + Node: Node{Path: "/etc/systemd/system/quux.service.d"}, + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "foo.service", + Contents: util.StrToPtr("[Foo]\nQux=Baz"), + Enabled: util.BoolToPtr(true), + }, + { + Name: "bar.service", + Dropins: []Dropin{ + { + Name: "baz.conf", + }, + }, + Enabled: util.BoolToPtr(true), + }, + { + Name: "qux.service", + Dropins: []Dropin{ + { + Name: "bar.conf", + Contents: util.StrToPtr("[Foo]\nQux=Baz"), + }, + }, + }, + { + Name: "quux.service", + Contents: util.StrToPtr("[Foo]\nQux=Baz"), + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + }, + } + for i, test := range tests { + r := test.in.Validate(path.New("json")) + expected := report.Report{} + expected.AddOnError(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad error: expected : %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_6_experimental/types/device.go b/config/v3_6_experimental/types/device.go new file mode 100644 index 000000000..a10ce97b0 --- /dev/null +++ b/config/v3_6_experimental/types/device.go @@ -0,0 +1,25 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d Device) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c, validatePath(string(d))) + return +} diff --git a/config/v3_6_experimental/types/directory.go b/config/v3_6_experimental/types/directory.go new file mode 100644 index 000000000..f6f068455 --- /dev/null +++ b/config/v3_6_experimental/types/directory.go @@ -0,0 +1,26 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + r.Merge(d.Node.Validate(c)) + r.AddOnError(c.Append("mode"), validateMode(d.Mode)) + return +} diff --git a/config/v3_6_experimental/types/disk.go b/config/v3_6_experimental/types/disk.go new file mode 100644 index 000000000..8caf8499d --- /dev/null +++ b/config/v3_6_experimental/types/disk.go @@ -0,0 +1,135 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d Disk) Key() string { + return d.Device +} + +func (n Disk) Validate(c path.ContextPath) (r report.Report) { + if len(n.Device) == 0 { + r.AddOnError(c.Append("device"), errors.ErrDiskDeviceRequired) + return + } + r.AddOnError(c.Append("device"), validatePath(n.Device)) + + if collides, p := n.partitionNumbersCollide(); collides { + r.AddOnError(c.Append("partitions", p), errors.ErrPartitionNumbersCollide) + } + if overlaps, p := n.partitionsOverlap(); overlaps { + r.AddOnError(c.Append("partitions", p), errors.ErrPartitionsOverlap) + } + if n.partitionsMixZeroesAndNonexistence() { + r.AddOnError(c.Append("partitions"), errors.ErrZeroesWithShouldNotExist) + } + if collides, p := n.partitionLabelsCollide(); collides { + r.AddOnError(c.Append("partitions", p), errors.ErrDuplicateLabels) + } + return +} + +// partitionNumbersCollide returns true if partition numbers in n.Partitions are not unique. It also returns the +// index of the colliding partition +func (n Disk) partitionNumbersCollide() (bool, int) { + m := map[int][]int{} // from partition number to index into array + for i, 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], i) + } + } + for _, n := range m { + if len(n) > 1 { + // TODO(vc): return information describing the collision for logging + return true, n[1] + } + } + return false, 0 +} + +func (d Disk) partitionLabelsCollide() (bool, int) { + m := map[string]struct{}{} + for i, p := range d.Partitions { + if p.Label != nil { + // a number of 0 means next available number, multiple devices can specify this + if _, exists := m[*p.Label]; exists { + return true, i + } + m[*p.Label] = struct{}{} + } + } + return false, 0 +} + +// end returns the last sector of a partition. Only used by partitionsOverlap. Requires non-nil Start and Size. +func (p Partition) end() int { + if *p.SizeMiB == 0 { + // a size of 0 means "fill available", just return the start as the end for those. + return *p.StartMiB + } + return *p.StartMiB + *p.SizeMiB - 1 +} + +// partitionsOverlap returns true if any explicitly dimensioned partitions overlap. It also returns the index of +// the overlapping partition +func (n Disk) partitionsOverlap() (bool, int) { + 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.StartMiB == nil || p.SizeMiB == nil || *p.StartMiB == 0 { + continue + } + + for i, o := range n.Partitions { + if o.StartMiB == nil || o.SizeMiB == nil || p == o || *o.StartMiB == 0 { + continue + } + + // is p.StartMiB within o? + if *p.StartMiB >= *o.StartMiB && *p.StartMiB <= o.end() { + return true, i + } + + // is p.end() within o? + if p.end() >= *o.StartMiB && p.end() <= o.end() { + return true, i + } + + // do p.StartMiB and p.end() straddle o? + if *p.StartMiB < *o.StartMiB && p.end() > o.end() { + return true, i + } + } + } + return false, 0 +} + +func (n Disk) partitionsMixZeroesAndNonexistence() bool { + hasZero := false + hasShouldNotExist := false + for _, p := range n.Partitions { + hasShouldNotExist = hasShouldNotExist || util.IsFalse(p.ShouldExist) + hasZero = hasZero || (p.Number == 0) + } + return hasZero && hasShouldNotExist +} diff --git a/config/v3_6_experimental/types/file.go b/config/v3_6_experimental/types/file.go new file mode 100644 index 000000000..9b71bb26a --- /dev/null +++ b/config/v3_6_experimental/types/file.go @@ -0,0 +1,43 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (f File) Validate(c path.ContextPath) (r report.Report) { + r.Merge(f.Node.Validate(c)) + r.AddOnError(c.Append("mode"), validateMode(f.Mode)) + r.AddOnError(c.Append("overwrite"), f.validateOverwrite()) + return +} + +func (f File) validateOverwrite() error { + if util.IsTrue(f.Overwrite) && f.Contents.Source == nil { + return errors.ErrOverwriteAndNilSource + } + return nil +} + +func (f FileEmbedded1) IgnoreDuplicates() map[string]struct{} { + return map[string]struct{}{ + "Append": {}, + } +} diff --git a/config/v3_6_experimental/types/file_test.go b/config/v3_6_experimental/types/file_test.go new file mode 100644 index 000000000..449e07009 --- /dev/null +++ b/config/v3_6_experimental/types/file_test.go @@ -0,0 +1,117 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func TestFileValidateOverwrite(t *testing.T) { + tests := []struct { + in File + out error + }{ + { + File{}, + nil, + }, + { + File{ + Node: Node{ + Overwrite: util.BoolToPtr(true), + }, + }, + errors.ErrOverwriteAndNilSource, + }, + { + File{ + Node: Node{ + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: FileEmbedded1{ + Contents: Resource{ + Source: util.StrToPtr(""), + }, + }, + }, + nil, + }, + { + File{ + Node: Node{ + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: FileEmbedded1{ + Contents: Resource{ + Source: util.StrToPtr("http://example.com"), + }, + }, + }, + nil, + }, + } + + for i, test := range tests { + err := test.in.validateOverwrite() + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} + +func TestFileContentsValidate(t *testing.T) { + tests := []struct { + in Resource + out error + }{ + { + Resource{}, + nil, + }, + { + Resource{ + Source: util.StrToPtr(""), + }, + nil, + }, + { + Resource{ + Source: util.StrToPtr(""), + Verification: Verification{ + Hash: util.StrToPtr(""), + }, + }, + nil, + }, + { + Resource{ + Verification: Verification{ + Hash: util.StrToPtr(""), + }, + }, + errors.ErrVerificationAndNilSource, + }, + } + + for i, test := range tests { + err := test.in.validateVerification() + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} diff --git a/config/v3_6_experimental/types/filesystem.go b/config/v3_6_experimental/types/filesystem.go new file mode 100644 index 000000000..c722b3633 --- /dev/null +++ b/config/v3_6_experimental/types/filesystem.go @@ -0,0 +1,106 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (f Filesystem) Key() string { + return f.Device +} + +func (f Filesystem) IgnoreDuplicates() map[string]struct{} { + return map[string]struct{}{ + "Options": {}, + "MountOptions": {}, + } +} + +func (f Filesystem) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c.Append("path"), f.validatePath()) + r.AddOnError(c.Append("device"), validatePath(f.Device)) + r.AddOnError(c.Append("format"), f.validateFormat()) + r.AddOnError(c.Append("label"), f.validateLabel()) + return +} + +func (f Filesystem) validatePath() error { + return validatePathNilOK(f.Path) +} + +func (f Filesystem) validateFormat() error { + if util.NilOrEmpty(f.Format) { + if util.NotEmpty(f.Path) || + util.NotEmpty(f.Label) || + util.NotEmpty(f.UUID) || + util.IsTrue(f.WipeFilesystem) || + len(f.MountOptions) != 0 || + len(f.Options) != 0 { + return errors.ErrFormatNilWithOthers + } + } else { + switch *f.Format { + case "ext4", "btrfs", "xfs", "swap", "vfat", "none": + default: + return errors.ErrFilesystemInvalidFormat + } + } + return nil +} + +func (f Filesystem) validateLabel() error { + if util.NilOrEmpty(f.Label) { + return nil + } + if util.NilOrEmpty(f.Format) { + return errors.ErrLabelNeedsFormat + } + + switch *f.Format { + case "ext4": + if len(*f.Label) > 16 { + // source: man mkfs.ext4 + return errors.ErrExt4LabelTooLong + } + case "btrfs": + if len(*f.Label) > 256 { + // source: man mkfs.btrfs + return errors.ErrBtrfsLabelTooLong + } + case "xfs": + if len(*f.Label) > 12 { + // source: man mkfs.xfs + return errors.ErrXfsLabelTooLong + } + 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(*f.Label) > 15 { + return errors.ErrSwapLabelTooLong + } + case "vfat": + if len(*f.Label) > 11 { + // source: man mkfs.fat + return errors.ErrVfatLabelTooLong + } + } + return nil +} diff --git a/config/v3_6_experimental/types/filesystem_test.go b/config/v3_6_experimental/types/filesystem_test.go new file mode 100644 index 000000000..1f8db9037 --- /dev/null +++ b/config/v3_6_experimental/types/filesystem_test.go @@ -0,0 +1,194 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func TestFilesystemValidateFormat(t *testing.T) { + tests := []struct { + in Filesystem + out error + }{ + { + Filesystem{Format: util.StrToPtr("ext4")}, + nil, + }, + { + Filesystem{Format: util.StrToPtr("btrfs")}, + nil, + }, + { + Filesystem{Format: util.StrToPtr("")}, + nil, + }, + { + Filesystem{Format: nil}, + nil, + }, + { + Filesystem{Label: util.StrToPtr("z")}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{MountOptions: []MountOption{MountOption("z")}}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{Options: []FilesystemOption{FilesystemOption("z")}}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{Format: util.StrToPtr(""), Path: util.StrToPtr("/")}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{Format: nil, Path: util.StrToPtr("/")}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{UUID: util.StrToPtr("z")}, + errors.ErrFormatNilWithOthers, + }, + { + Filesystem{WipeFilesystem: util.BoolToPtr(true)}, + errors.ErrFormatNilWithOthers, + }, + } + + for i, test := range tests { + err := test.in.validateFormat() + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} + +func TestFilesystemValidatePath(t *testing.T) { + tests := []struct { + in Filesystem + out error + }{ + { + Filesystem{Path: util.StrToPtr("/foo")}, + nil, + }, + { + Filesystem{Path: util.StrToPtr("")}, + nil, + }, + { + Filesystem{Path: nil}, + nil, + }, + { + Filesystem{Path: util.StrToPtr("foo")}, + errors.ErrPathRelative, + }, + } + + for i, test := range tests { + err := test.in.validatePath() + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} + +func TestLabelValidate(t *testing.T) { + type in struct { + filesystem Filesystem + } + type out struct { + err error + } + + tests := []struct { + in in + out out + }{ + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("ext4"), Label: nil}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("ext4"), Label: util.StrToPtr("data")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("ext4"), Label: util.StrToPtr("thislabelistoolong")}}, + out: out{err: errors.ErrExt4LabelTooLong}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("btrfs"), Label: nil}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("btrfs"), Label: util.StrToPtr("thislabelisnottoolong")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("btrfs"), Label: util.StrToPtr("thislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolongthislabelistoolong")}}, + out: out{err: errors.ErrBtrfsLabelTooLong}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("xfs"), Label: nil}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("xfs"), Label: util.StrToPtr("data")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("xfs"), Label: util.StrToPtr("thislabelistoolong")}}, + out: out{err: errors.ErrXfsLabelTooLong}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("swap"), Label: nil}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("swap"), Label: util.StrToPtr("data")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("swap"), Label: util.StrToPtr("thislabelistoolong")}}, + out: out{err: errors.ErrSwapLabelTooLong}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("vfat"), Label: nil}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("vfat"), Label: util.StrToPtr("data")}}, + out: out{}, + }, + { + in: in{filesystem: Filesystem{Format: util.StrToPtr("vfat"), Label: util.StrToPtr("thislabelistoolong")}}, + out: out{err: errors.ErrVfatLabelTooLong}, + }, + } + + for i, test := range tests { + err := test.in.filesystem.validateLabel() + if test.out.err != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out.err, err) + } + } +} diff --git a/config/v3_6_experimental/types/headers.go b/config/v3_6_experimental/types/headers.go new file mode 100644 index 000000000..be1aadad9 --- /dev/null +++ b/config/v3_6_experimental/types/headers.go @@ -0,0 +1,65 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "net/http" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Parse generates standard net/http headers from the data in HTTPHeaders +func (hs HTTPHeaders) Parse() (http.Header, error) { + headers := http.Header{} + for _, header := range hs { + if header.Name == "" { + return nil, errors.ErrEmptyHTTPHeaderName + } + if header.Value == nil || string(*header.Value) == "" { + return nil, errors.ErrInvalidHTTPHeader + } + headers.Add(header.Name, string(*header.Value)) + } + return headers, nil +} + +func (h HTTPHeader) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c.Append("name"), h.validateName()) + r.AddOnError(c.Append("value"), h.validateValue()) + return +} + +func (h HTTPHeader) validateName() error { + if h.Name == "" { + return errors.ErrEmptyHTTPHeaderName + } + return nil +} + +func (h HTTPHeader) validateValue() error { + if h.Value == nil { + return nil + } + if string(*h.Value) == "" { + return errors.ErrInvalidHTTPHeader + } + return nil +} + +func (h HTTPHeader) Key() string { + return h.Name +} diff --git a/config/v3_6_experimental/types/headers_test.go b/config/v3_6_experimental/types/headers_test.go new file mode 100644 index 000000000..40380b684 --- /dev/null +++ b/config/v3_6_experimental/types/headers_test.go @@ -0,0 +1,159 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" +) + +func toPointer(val string) *string { + return &val +} + +func equal(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func TestHeadersParse(t *testing.T) { + tests := []struct { + in HTTPHeaders + out error + }{ + { + // Valid headers + HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Name: "header2", + Value: toPointer("header2value"), + }, + }, + nil, + }, + { + // Duplicate headers + HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Name: "header1", + Value: toPointer("header2value"), + }, + }, + nil, + }, + { + // No header name + HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Value: toPointer("emptyheadervalue"), + }, + }, + errors.ErrEmptyHTTPHeaderName, + }, + { + // No header value + HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Name: "emptyheadername", + }, + }, + errors.ErrInvalidHTTPHeader, + }, + { + // Invalid header without elements + HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{}, + }, + errors.ErrEmptyHTTPHeaderName, + }, + } + + for i, test := range tests { + _, err := test.in.Parse() + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} + +func TestValidHeadersParse(t *testing.T) { + // Valid headers + headers := HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Name: "header2", + Value: toPointer("header2value"), + }, + } + parseHeaders, err := headers.Parse() + if err != nil { + t.Errorf("error during parsing valid headers: %v", err) + } + if !equal(parseHeaders["Header1"], []string{"header1value"}) || !equal(parseHeaders["Header2"], []string{"header2value"}) { + t.Errorf("parsed HTTP headers values are wrong") + } +} + +func TestDuplicateHeadersParse(t *testing.T) { + // Duplicate headers + headers := HTTPHeaders{ + HTTPHeader{ + Name: "header1", + Value: toPointer("header1value"), + }, + HTTPHeader{ + Name: "header1", + Value: toPointer("header2value"), + }, + } + parseHeaders, err := headers.Parse() + if err != nil { + t.Errorf("error during parsing valid headers: %v", err) + } + if !equal(parseHeaders["Header1"], []string{"header1value", "header2value"}) { + t.Errorf("parsed HTTP headers values are wrong") + } +} diff --git a/config/v3_6_experimental/types/ignition.go b/config/v3_6_experimental/types/ignition.go new file mode 100644 index 000000000..190445bda --- /dev/null +++ b/config/v3_6_experimental/types/ignition.go @@ -0,0 +1,49 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/go-semver/semver" + + "github.com/coreos/ignition/v2/config/shared/errors" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (v Ignition) Semver() (*semver.Version, error) { + return semver.NewVersion(v.Version) +} + +func (ic IgnitionConfig) Validate(c path.ContextPath) (r report.Report) { + for i, res := range ic.Merge { + r.AddOnError(c.Append("merge", i), res.validateRequiredSource()) + } + return +} + +func (v Ignition) Validate(c path.ContextPath) (r report.Report) { + c = c.Append("version") + tv, err := v.Semver() + if err != nil { + r.AddOnError(c, errors.ErrInvalidVersion) + return + } + + if MaxVersion != *tv { + r.AddOnError(c, errors.ErrUnknownVersion) + } + return +} diff --git a/config/v3_6_experimental/types/ignition_test.go b/config/v3_6_experimental/types/ignition_test.go new file mode 100644 index 000000000..ac7d462d3 --- /dev/null +++ b/config/v3_6_experimental/types/ignition_test.go @@ -0,0 +1,48 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/vcontext/validate" +) + +func TestIgnitionConfigValidate(t *testing.T) { + tests := []struct { + in IgnitionConfig + out string + }{ + { + IgnitionConfig{ + Merge: []Resource{{}}, + }, + "error at $.merge.0: source is required\n", + }, + { + IgnitionConfig{ + Replace: Resource{}, + }, + "", + }, + } + + for i, test := range tests { + r := validate.Validate(test.in, "test") + if test.out != r.String() { + t.Errorf("#%d: bad error: want %q, got %q", i, test.out, r.String()) + } + } +} diff --git a/config/v3_6_experimental/types/kargs.go b/config/v3_6_experimental/types/kargs.go new file mode 100644 index 000000000..42c29408e --- /dev/null +++ b/config/v3_6_experimental/types/kargs.go @@ -0,0 +1,22 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +func (k KernelArguments) MergedKeys() map[string]string { + return map[string]string{ + "ShouldExist": "KernelArgument", + "ShouldNotExist": "KernelArgument", + } +} diff --git a/config/v3_6_experimental/types/kargs_test.go b/config/v3_6_experimental/types/kargs_test.go new file mode 100644 index 000000000..1a258727e --- /dev/null +++ b/config/v3_6_experimental/types/kargs_test.go @@ -0,0 +1,51 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/validate" +) + +func TestKernelArgumentsValidate(t *testing.T) { + tests := []struct { + in KernelArguments + out string + }{ + // Ensure that ValidateWithContext prevents duplicate entries + // in ShouldExist & ShouldNotExist + { + KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + "bar", + }, + ShouldNotExist: []KernelArgument{ + "baz", + "foo", + }, + }, + "error at $.shouldNotExist.1: duplicate entry defined\n", + }, + } + + for i, test := range tests { + r := validate.ValidateWithContext(test.in, nil) + if test.out != r.String() { + t.Errorf("#%d: bad error: want %q, got %q", i, test.out, r.String()) + } + } +} diff --git a/config/v3_6_experimental/types/luks.go b/config/v3_6_experimental/types/luks.go new file mode 100644 index 000000000..e4c1d6815 --- /dev/null +++ b/config/v3_6_experimental/types/luks.go @@ -0,0 +1,82 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "strings" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (l Luks) Key() string { + return l.Name +} + +func (l Luks) IgnoreDuplicates() map[string]struct{} { + return map[string]struct{}{ + "Options": {}, + } +} + +func (l Luks) Validate(c path.ContextPath) (r report.Report) { + if strings.Contains(l.Name, "/") { + r.AddOnError(c.Append("name"), errors.ErrLuksNameContainsSlash) + } + r.AddOnError(c.Append("label"), l.validateLabel()) + if util.NilOrEmpty(l.Device) { + r.AddOnError(c.Append("device"), errors.ErrDiskDeviceRequired) + } else { + r.AddOnError(c.Append("device"), validatePath(*l.Device)) + } + + if util.NotEmpty(l.Clevis.Custom.Pin) && (len(l.Clevis.Tang) > 0 || util.IsTrue(l.Clevis.Tpm2) || (l.Clevis.Threshold != nil && *l.Clevis.Threshold != 0)) { + r.AddOnError(c.Append("clevis"), errors.ErrClevisCustomWithOthers) + } + + // fail if a key file is provided and is not valid + if err := validateURLNilOK(l.KeyFile.Source); err != nil { + r.AddOnError(c.Append("keys"), errors.ErrInvalidLuksKeyFile) + } + + // fail if Cex use with Clevis + if l.Clevis.IsPresent() && l.Cex.IsPresent() { + r.AddOnError(c.Append("cex"), errors.ErrCexWithClevis) + } + + // fail if key file is provided along with Cex + if l.Cex.IsPresent() && util.NotEmpty(l.KeyFile.Source) { + r.AddOnError(c.Append("cex"), errors.ErrCexWithKeyFile) + } + + return +} + +func (l Luks) validateLabel() error { + if util.NilOrEmpty(l.Label) { + return nil + } + + if len(*l.Label) > 47 { + // LUKS2_LABEL_L has a maximum length of 48 (including the null terminator) + // https://gitlab.com/cryptsetup/cryptsetup/-/blob/1633f030e89ad2f11ae649ba9600997a41abd3fc/lib/luks2/luks2.h#L86 + return errors.ErrLuksLabelTooLong + } + + return nil +} diff --git a/config/v3_6_experimental/types/mode.go b/config/v3_6_experimental/types/mode.go new file mode 100644 index 000000000..9eb7573d8 --- /dev/null +++ b/config/v3_6_experimental/types/mode.go @@ -0,0 +1,26 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" +) + +func validateMode(m *int) error { + if m != nil && (*m < 0 || *m > 07777) { + return errors.ErrFileIllegalMode + } + return nil +} diff --git a/config/v3_6_experimental/types/mode_test.go b/config/v3_6_experimental/types/mode_test.go new file mode 100644 index 000000000..8966e25d8 --- /dev/null +++ b/config/v3_6_experimental/types/mode_test.go @@ -0,0 +1,62 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func TestModeValidate(t *testing.T) { + tests := []struct { + in *int + out error + }{ + { + nil, + nil, + }, + { + util.IntToPtr(0), + nil, + }, + { + util.IntToPtr(0644), + nil, + }, + { + util.IntToPtr(01755), + nil, + }, + { + util.IntToPtr(07777), + nil, + }, + { + util.IntToPtr(010000), + errors.ErrFileIllegalMode, + }, + } + + for i, test := range tests { + err := validateMode(test.in) + if !reflect.DeepEqual(test.out, err) { + t.Errorf("#%d: bad err: want %v, got %v", i, test.out, err) + } + } +} diff --git a/config/v3_6_experimental/types/node.go b/config/v3_6_experimental/types/node.go new file mode 100644 index 000000000..248276e73 --- /dev/null +++ b/config/v3_6_experimental/types/node.go @@ -0,0 +1,59 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "path" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + vpath "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (n Node) Key() string { + return n.Path +} + +func (n Node) Validate(c vpath.ContextPath) (r report.Report) { + r.AddOnError(c.Append("path"), validatePath(n.Path)) + return +} + +func (n Node) Depth() int { + count := 0 + for p := path.Clean(string(n.Path)); p != "/"; count++ { + p = path.Dir(p) + } + return count +} + +func validateIDorName(id *int, name *string) error { + if id != nil && util.NotEmpty(name) { + return errors.ErrBothIDAndNameSet + } + return nil +} + +func (nu NodeUser) Validate(c vpath.ContextPath) (r report.Report) { + r.AddOnError(c, validateIDorName(nu.ID, nu.Name)) + return +} + +func (ng NodeGroup) Validate(c vpath.ContextPath) (r report.Report) { + r.AddOnError(c, validateIDorName(ng.ID, ng.Name)) + return +} diff --git a/config/v3_6_experimental/types/node_test.go b/config/v3_6_experimental/types/node_test.go new file mode 100644 index 000000000..20ba4eb67 --- /dev/null +++ b/config/v3_6_experimental/types/node_test.go @@ -0,0 +1,101 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestNodeValidatePath(t *testing.T) { + node := Node{Path: "not/absolute"} + rep := report.Report{} + rep.AddOnError(path.ContextPath{}.Append("path"), errors.ErrPathRelative) + if receivedRep := node.Validate(path.ContextPath{}); !reflect.DeepEqual(rep, receivedRep) { + t.Errorf("bad error: want %v, got %v", rep, receivedRep) + } +} + +func TestNodeValidateUser(t *testing.T) { + tests := []struct { + in NodeUser + out error + }{ + { + NodeUser{util.IntToPtr(0), util.StrToPtr("")}, + nil, + }, + { + NodeUser{util.IntToPtr(1000), util.StrToPtr("")}, + nil, + }, + { + NodeUser{nil, util.StrToPtr("core")}, + nil, + }, + { + NodeUser{util.IntToPtr(1000), util.StrToPtr("core")}, + errors.ErrBothIDAndNameSet, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(path.New(""), test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v got %v", i, test.out, r) + } + } +} + +func TestNodeValidateGroup(t *testing.T) { + tests := []struct { + in NodeGroup + out error + }{ + { + NodeGroup{util.IntToPtr(0), util.StrToPtr("")}, + nil, + }, + { + NodeGroup{util.IntToPtr(1000), util.StrToPtr("")}, + nil, + }, + { + NodeGroup{nil, util.StrToPtr("core")}, + nil, + }, + { + NodeGroup{util.IntToPtr(1000), util.StrToPtr("core")}, + errors.ErrBothIDAndNameSet, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(path.New(""), test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v got %v", i, test.out, r) + } + } +} diff --git a/config/v3_6_experimental/types/partition.go b/config/v3_6_experimental/types/partition.go new file mode 100644 index 000000000..1b2d97edf --- /dev/null +++ b/config/v3_6_experimental/types/partition.go @@ -0,0 +1,91 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "fmt" + "regexp" + "strings" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + guidRegexStr = "^(|[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})$" +) + +var ( + guidRegex = regexp.MustCompile(guidRegexStr) +) + +func (p Partition) Key() string { + if p.Number != 0 { + return fmt.Sprintf("number:%d", p.Number) + } else if p.Label != nil { + return fmt.Sprintf("label:%s", *p.Label) + } else { + return "" + } +} + +func (p Partition) Validate(c path.ContextPath) (r report.Report) { + if util.IsFalse(p.ShouldExist) && + (p.Label != nil || util.NotEmpty(p.TypeGUID) || util.NotEmpty(p.GUID) || p.StartMiB != nil || p.SizeMiB != nil) { + r.AddOnError(c, errors.ErrShouldNotExistWithOthers) + } + if p.Number == 0 && p.Label == nil { + r.AddOnError(c, errors.ErrNeedLabelOrNumber) + } + + r.AddOnError(c.Append("label"), p.validateLabel()) + r.AddOnError(c.Append("guid"), validateGUID(p.GUID)) + r.AddOnError(c.Append("typeGuid"), validateGUID(p.TypeGUID)) + return +} + +func (p Partition) validateLabel() error { + if p.Label == nil { + return nil + } + // 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 { + return errors.ErrLabelTooLong + } + + // sgdisk uses colons for delimitting compound arguments and does not allow escaping them. + if strings.Contains(*p.Label, ":") { + return errors.ErrLabelContainsColon + } + return nil +} + +func validateGUID(guidPointer *string) error { + if guidPointer == nil { + return nil + } + guid := *guidPointer + if ok := guidRegex.MatchString(guid); !ok { + return errors.ErrDoesntMatchGUIDRegex + } + return nil +} diff --git a/config/v3_6_experimental/types/partition_test.go b/config/v3_6_experimental/types/partition_test.go new file mode 100644 index 000000000..a1e1623f4 --- /dev/null +++ b/config/v3_6_experimental/types/partition_test.go @@ -0,0 +1,94 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func TestValidateLabel(t *testing.T) { + tests := []struct { + in *string + out error + }{ + { + util.StrToPtr("root"), + nil, + }, + { + util.StrToPtr(""), + nil, + }, + { + nil, + nil, + }, + { + util.StrToPtr("111111111111111111111111111111111111"), + nil, + }, + { + util.StrToPtr("1111111111111111111111111111111111111"), + errors.ErrLabelTooLong, + }, + { + util.StrToPtr("test:"), + errors.ErrLabelContainsColon, + }, + } + for i, test := range tests { + err := Partition{Label: test.in}.validateLabel() + if err != test.out { + t.Errorf("#%d: wanted %v, got %v", i, test.out, err) + } + } +} + +func TestValidateGUID(t *testing.T) { + tests := []struct { + in *string + out error + }{ + { + util.StrToPtr("5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6"), + nil, + }, + { + util.StrToPtr("5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6"), + nil, + }, + { + util.StrToPtr(""), + nil, + }, + { + nil, + nil, + }, + { + util.StrToPtr("not-a-valid-typeguid"), + errors.ErrDoesntMatchGUIDRegex, + }, + } + for i, test := range tests { + err := validateGUID(test.in) + if err != test.out { + t.Errorf("#%d: wanted %v, got %v", i, test.out, err) + } + } +} diff --git a/config/v3_6_experimental/types/passwd.go b/config/v3_6_experimental/types/passwd.go new file mode 100644 index 000000000..4060a2a6f --- /dev/null +++ b/config/v3_6_experimental/types/passwd.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +func (p PasswdUser) Key() string { + return p.Name +} + +func (g PasswdGroup) Key() string { + return g.Name +} diff --git a/config/v3_6_experimental/types/path.go b/config/v3_6_experimental/types/path.go new file mode 100644 index 000000000..131e300c1 --- /dev/null +++ b/config/v3_6_experimental/types/path.go @@ -0,0 +1,42 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "path" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func validatePath(p string) error { + if p == "" { + return errors.ErrNoPath + } + if !path.IsAbs(p) { + return errors.ErrPathRelative + } + if path.Clean(p) != p { + return errors.ErrDirtyPath + } + return nil +} + +func validatePathNilOK(p *string) error { + if util.NilOrEmpty(p) { + return nil + } + return validatePath(*p) +} diff --git a/config/v3_6_experimental/types/path_test.go b/config/v3_6_experimental/types/path_test.go new file mode 100644 index 000000000..3d199cbdf --- /dev/null +++ b/config/v3_6_experimental/types/path_test.go @@ -0,0 +1,57 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" +) + +func TestPathValidate(t *testing.T) { + tests := []struct { + in string + out error + }{ + { + "/good/path", + nil, + }, + { + "/name", + nil, + }, + { + "/this/is/a/fairly/long/path/to/a/device.", + nil, + }, + { + "/this one has spaces", + nil, + }, + { + "relative/path", + errors.ErrPathRelative, + }, + } + + for i, test := range tests { + err := validatePath(test.in) + if !reflect.DeepEqual(test.out, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} diff --git a/config/v3_6_experimental/types/proxy.go b/config/v3_6_experimental/types/proxy.go new file mode 100644 index 000000000..d48d210a0 --- /dev/null +++ b/config/v3_6_experimental/types/proxy.go @@ -0,0 +1,49 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "net/url" + + "github.com/coreos/ignition/v2/config/shared/errors" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (p Proxy) Validate(c path.ContextPath) (r report.Report) { + validateProxyURL(p.HTTPProxy, c.Append("httpProxy"), &r, true) + validateProxyURL(p.HTTPSProxy, c.Append("httpsProxy"), &r, false) + return +} + +func validateProxyURL(s *string, p path.ContextPath, r *report.Report, httpOk bool) { + if s == nil { + return + } + u, err := url.Parse(*s) + if err != nil { + r.AddOnError(p, errors.ErrInvalidUrl) + return + } + + if u.Scheme != "https" && u.Scheme != "http" { + r.AddOnError(p, errors.ErrInvalidProxy) + return + } + if u.Scheme == "http" && !httpOk { + r.AddOnWarn(p, errors.ErrInsecureProxy) + } +} diff --git a/config/v3_6_experimental/types/proxy_test.go b/config/v3_6_experimental/types/proxy_test.go new file mode 100644 index 000000000..269f4fb22 --- /dev/null +++ b/config/v3_6_experimental/types/proxy_test.go @@ -0,0 +1,112 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestValidateProxyURL(t *testing.T) { + tests := []struct { + in *string + httpOk bool + out report.Entry + }{ + { + nil, + false, + report.Entry{}, + }, + { + nil, + true, + report.Entry{}, + }, + { + util.StrToPtr("https://example.com"), + false, + report.Entry{}, + }, + { + util.StrToPtr("https://example.com"), + true, + report.Entry{}, + }, + { + util.StrToPtr("http://example.com"), + false, + report.Entry{ + Kind: report.Warn, + Message: errors.ErrInsecureProxy.Error(), + }, + }, + { + util.StrToPtr("http://example.com"), + true, + report.Entry{}, + }, + { + util.StrToPtr("ftp://example.com"), + false, + report.Entry{ + Kind: report.Error, + Message: errors.ErrInvalidProxy.Error(), + }, + }, + { + util.StrToPtr("ftp://example.com"), + true, + report.Entry{ + Kind: report.Error, + Message: errors.ErrInvalidProxy.Error(), + }, + }, + { + util.StrToPtr("http://[::1]a"), + false, + report.Entry{ + Kind: report.Error, + Message: errors.ErrInvalidUrl.Error(), + }, + }, + { + util.StrToPtr("http://[::1]a"), + true, + report.Entry{ + Kind: report.Error, + Message: errors.ErrInvalidUrl.Error(), + }, + }, + } + + for i, test := range tests { + r := report.Report{} + validateProxyURL(test.in, path.New(""), &r, test.httpOk) + e := report.Entry{} + if len(r.Entries) > 0 { + e = r.Entries[0] + } + if !reflect.DeepEqual(test.out, e) { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, e) + } + } +} diff --git a/config/v3_6_experimental/types/raid.go b/config/v3_6_experimental/types/raid.go new file mode 100644 index 000000000..9d69aa366 --- /dev/null +++ b/config/v3_6_experimental/types/raid.go @@ -0,0 +1,62 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (r Raid) Key() string { + return r.Name +} + +func (r Raid) IgnoreDuplicates() map[string]struct{} { + return map[string]struct{}{ + "Options": {}, + } +} + +func (ra Raid) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c.Append("level"), ra.validateLevel()) + if len(ra.Devices) == 0 { + r.AddOnError(c.Append("devices"), errors.ErrRaidDevicesRequired) + } + return +} + +func (r Raid) validateLevel() error { + if util.NilOrEmpty(r.Level) { + return errors.ErrRaidLevelRequired + } + switch *r.Level { + case "linear", "raid0", "0", "stripe": + if r.Spares != nil && *r.Spares != 0 { + return errors.ErrSparesUnsupportedForLevel + } + case "raid1", "1", "mirror": + case "raid4", "4": + case "raid5", "5": + case "raid6", "6": + case "raid10", "10": + default: + return errors.ErrUnrecognizedRaidLevel + } + + return nil +} diff --git a/config/v3_6_experimental/types/raid_test.go b/config/v3_6_experimental/types/raid_test.go new file mode 100644 index 000000000..7323b26fb --- /dev/null +++ b/config/v3_6_experimental/types/raid_test.go @@ -0,0 +1,88 @@ +// Copyright 2021 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestRaidValidate(t *testing.T) { + tests := []struct { + in Raid + at path.ContextPath + out error + }{ + { + in: Raid{ + Name: "name", + Level: util.StrToPtr("0"), + Devices: []Device{"/dev/fd0"}, + Spares: util.IntToPtr(0), + }, + out: nil, + }, + { + in: Raid{ + Name: "name", + Devices: []Device{"/dev/fd0"}, + }, + at: path.New("", "level"), + out: errors.ErrRaidLevelRequired, + }, + { + in: Raid{ + Name: "name", + Level: util.StrToPtr("0"), + Devices: []Device{"/dev/fd0"}, + Spares: util.IntToPtr(1), + }, + at: path.New("", "level"), + out: errors.ErrSparesUnsupportedForLevel, + }, + { + in: Raid{ + Name: "name", + Devices: []Device{"/dev/fd0"}, + Level: util.StrToPtr("zzz"), + }, + at: path.New("", "level"), + out: errors.ErrUnrecognizedRaidLevel, + }, + { + in: Raid{ + Name: "name", + Level: util.StrToPtr("0"), + }, + at: path.New("", "devices"), + out: errors.ErrRaidDevicesRequired, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_6_experimental/types/resource.go b/config/v3_6_experimental/types/resource.go new file mode 100644 index 000000000..68da6c7b7 --- /dev/null +++ b/config/v3_6_experimental/types/resource.go @@ -0,0 +1,91 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "net/url" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (res Resource) Key() string { + if res.Source == nil { + return "" + } + return *res.Source +} + +func (res Resource) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c.Append("compression"), res.validateCompression()) + r.AddOnError(c.Append("verification", "hash"), res.validateVerification()) + r.AddOnError(c.Append("source"), validateURLNilOK(res.Source)) + r.AddOnError(c.Append("httpHeaders"), res.validateSchemeForHTTPHeaders()) + return +} + +func (res Resource) validateCompression() error { + if res.Compression != nil { + switch *res.Compression { + case "", "gzip": + default: + return errors.ErrCompressionInvalid + } + } + return nil +} + +func (res Resource) validateVerification() error { + if res.Verification.Hash != nil && res.Source == nil { + return errors.ErrVerificationAndNilSource + } + return nil +} + +func (res Resource) validateSchemeForHTTPHeaders() error { + if len(res.HTTPHeaders) < 1 { + return nil + } + + if util.NilOrEmpty(res.Source) { + return errors.ErrInvalidUrl + } + + u, err := url.Parse(*res.Source) + if err != nil { + return errors.ErrInvalidUrl + } + + switch u.Scheme { + case "http", "https": + return nil + default: + return errors.ErrUnsupportedSchemeForHTTPHeaders + } +} + +// Ensure that the Source is specified and valid. This is not called by +// Resource.Validate() because some structs that embed Resource don't +// require Source to be specified. Containing structs that require Source +// should call this function from their Validate(). +func (res Resource) validateRequiredSource() error { + if util.NilOrEmpty(res.Source) { + return errors.ErrSourceRequired + } + return validateURL(*res.Source) +} diff --git a/config/v3_5_experimental/types/schema.go b/config/v3_6_experimental/types/schema.go similarity index 98% rename from config/v3_5_experimental/types/schema.go rename to config/v3_6_experimental/types/schema.go index 829578384..c652d666d 100644 --- a/config/v3_5_experimental/types/schema.go +++ b/config/v3_6_experimental/types/schema.go @@ -1,6 +1,6 @@ package types -// generated by "schematyper --package=types config/v3_5_experimental/schema/ignition.json -o config/v3_5_experimental/types/schema.go --root-type=Config" -- DO NOT EDIT +// generated by "schematyper --package=types config/v3_6_experimental/schema/ignition.json -o config/v3_6_experimental/types/schema.go --root-type=Config" -- DO NOT EDIT type Cex struct { Enabled *bool `json:"enabled,omitempty"` diff --git a/config/v3_6_experimental/types/storage.go b/config/v3_6_experimental/types/storage.go new file mode 100644 index 000000000..20cb73048 --- /dev/null +++ b/config/v3_6_experimental/types/storage.go @@ -0,0 +1,115 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "path" + "strings" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + vpath "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (s Storage) MergedKeys() map[string]string { + return map[string]string{ + "Directories": "Node", + "Files": "Node", + "Links": "Node", + } +} + +func (s Storage) Validate(c vpath.ContextPath) (r report.Report) { + s.validateDirectories(c, &r) + s.validateFiles(c, &r) + s.validateLinks(c, &r) + s.validateFilesystems(c, &r) + return +} + +func (s Storage) validateDirectories(c vpath.ContextPath, r *report.Report) { + for i, d := range s.Directories { + for _, l := range s.Links { + if strings.HasPrefix(d.Path, l.Path+"/") { + r.AddOnError(c.Append("directories", i), errors.ErrDirectoryUsedSymlink) + } + } + } +} + +func (s Storage) validateFiles(c vpath.ContextPath, r *report.Report) { + for i, f := range s.Files { + for _, l := range s.Links { + if strings.HasPrefix(f.Path, l.Path+"/") { + r.AddOnError(c.Append("files", i), errors.ErrFileUsedSymlink) + } + } + } +} + +func (s Storage) validateLinks(c vpath.ContextPath, r *report.Report) { + for i, l1 := range s.Links { + for _, l2 := range s.Links { + if strings.HasPrefix(l1.Path, l2.Path+"/") { + r.AddOnError(c.Append("links", i), errors.ErrLinkUsedSymlink) + } + } + if util.NilOrEmpty(l1.Target) { + r.AddOnError(c.Append("links", i, "target"), errors.ErrLinkTargetRequired) + continue + } + if !util.IsTrue(l1.Hard) { + continue + } + target := path.Clean(*l1.Target) + if !path.IsAbs(target) { + target = path.Join(l1.Path, *l1.Target) + } + for _, d := range s.Directories { + if target == d.Path { + r.AddOnError(c.Append("links", i), errors.ErrHardLinkToDirectory) + } + } + ownerCheck := func(ok bool, path vpath.ContextPath) { + if !ok { + r.AddOnWarn(path, errors.ErrHardLinkSpecifiesOwner) + } + } + ownerCheck(l1.User.ID == nil, c.Append("links", i, "user", "id")) + ownerCheck(l1.User.Name == nil, c.Append("links", i, "user", "name")) + ownerCheck(l1.Group.ID == nil, c.Append("links", i, "group", "id")) + ownerCheck(l1.Group.Name == nil, c.Append("links", i, "group", "name")) + } +} + +func (s Storage) validateFilesystems(c vpath.ContextPath, r *report.Report) { + disks := make(map[string]Disk) + for _, d := range s.Disks { + disks[d.Device] = d + } + + for i, f := range s.Filesystems { + disk, exist := disks[f.Device] + if exist { + if len(disk.Partitions) > 0 { + r.AddOnWarn(c.Append("filesystems", i, "device"), errors.ErrPartitionsOverwritten) + } else if !util.IsTrue(f.WipeFilesystem) && util.IsTrue(disk.WipeTable) { + r.AddOnWarn(c.Append("filesystems", i, "device"), errors.ErrFilesystemImplicitWipe) + } + } + } +} diff --git a/config/v3_6_experimental/types/storage_test.go b/config/v3_6_experimental/types/storage_test.go new file mode 100644 index 000000000..551688a27 --- /dev/null +++ b/config/v3_6_experimental/types/storage_test.go @@ -0,0 +1,359 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestStorageValidateErrors(t *testing.T) { + tests := []struct { + in Storage + at path.ContextPath + err error + warn error + }{ + // test empty storage config returns nil + { + in: Storage{}, + }, + // test a storage config with no conflicting paths returns nil + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-t")}, + }, + { + Node: Node{Path: "/quux"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/quux-t")}, + }, + }, + Files: []File{ + { + Node: Node{Path: "/bar"}, + }, + }, + Directories: []Directory{ + { + Node: Node{Path: "/baz"}, + }, + }, + }, + }, + // test when a file uses a configured symlink path returns ErrFileUsedSymlink + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-t")}, + }, + }, + Files: []File{ + { + Node: Node{Path: "/foo/bar"}, + }, + }, + }, + err: errors.ErrFileUsedSymlink, + at: path.New("", "files", 0), + }, + // test when a directory uses a configured symlink path returns ErrDirectoryUsedSymlink + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-t")}, + }, + }, + Directories: []Directory{ + { + Node: Node{Path: "/foo/bar"}, + }, + }, + }, + err: errors.ErrDirectoryUsedSymlink, + at: path.New("", "directories", 0), + }, + // test the same path listed for two separate symlinks returns ErrLinkUsedSymlink + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-t")}, + }, + { + Node: Node{Path: "/foo/bar"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-bar-t")}, + }, + }, + }, + err: errors.ErrLinkUsedSymlink, + at: path.New("", "links", 1), + }, + // test a configured symlink with no target returns ErrLinkTargetRequired + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Hard: util.BoolToPtr(true)}, + }, + }, + }, + err: errors.ErrLinkTargetRequired, + at: path.New("", "links", 0, "target"), + }, + // test a configured symlink with a nil target returns ErrLinkTargetRequired + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("")}, + }, + }, + }, + err: errors.ErrLinkTargetRequired, + at: path.New("", "links", 0, "target"), + }, + // test that two symlinks can be configured at a time + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/foo"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foo-t")}, + }, + { + Node: Node{Path: "/foob/bar"}, + LinkEmbedded1: LinkEmbedded1{Target: util.StrToPtr("/foob-bar-t")}, + }, + }, + }, + }, + // test when a directory uses a configured symlink with the 'Hard:= true' returns ErrHardLinkToDirectory + { + in: Storage{ + Links: []Link{ + { + Node: Node{Path: "/quux"}, + LinkEmbedded1: LinkEmbedded1{ + Target: util.StrToPtr("/foo/bar"), + Hard: util.BoolToPtr(true), + }, + }, + }, + Directories: []Directory{ + { + Node: Node{Path: "/foo/bar"}, + }, + }, + }, + err: errors.ErrHardLinkToDirectory, + at: path.New("", "links", 0), + }, + { + in: Storage{ + Links: []Link{ + { + Node: Node{ + Path: "/quux", + User: NodeUser{ + ID: util.IntToPtr(10), + }, + }, + LinkEmbedded1: LinkEmbedded1{ + Target: util.StrToPtr("/foo/bar"), + Hard: util.BoolToPtr(true), + }, + }, + }, + }, + warn: errors.ErrHardLinkSpecifiesOwner, + at: path.New("", "links", 0, "user", "id"), + }, + { + in: Storage{ + Links: []Link{ + { + Node: Node{ + Path: "/quux", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: LinkEmbedded1{ + Target: util.StrToPtr("/foo/bar"), + Hard: util.BoolToPtr(true), + }, + }, + }, + }, + warn: errors.ErrHardLinkSpecifiesOwner, + at: path.New("", "links", 0, "user", "name"), + }, + { + in: Storage{ + Links: []Link{ + { + Node: Node{ + Path: "/quux", + Group: NodeGroup{ + ID: util.IntToPtr(10), + }, + }, + LinkEmbedded1: LinkEmbedded1{ + Target: util.StrToPtr("/foo/bar"), + Hard: util.BoolToPtr(true), + }, + }, + }, + }, + warn: errors.ErrHardLinkSpecifiesOwner, + at: path.New("", "links", 0, "group", "id"), + }, + { + in: Storage{ + Links: []Link{ + { + Node: Node{ + Path: "/quux", + Group: NodeGroup{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: LinkEmbedded1{ + Target: util.StrToPtr("/foo/bar"), + Hard: util.BoolToPtr(true), + }, + }, + }, + }, + warn: errors.ErrHardLinkSpecifiesOwner, + at: path.New("", "links", 0, "group", "name"), + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(test.at, test.err) + expected.AddOnWarn(test.at, test.warn) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v, got %v", i, expected, r) + } + } +} + +func TestStorageValidateWarnings(t *testing.T) { + tests := []struct { + in Storage + at path.ContextPath + out error + }{ + // test a disk with partitions with the same 'device' as a filesystem returns ErrPartitionsOverwritten + { + in: Storage{ + Disks: []Disk{ + { + Device: "/dev/sda", + Partitions: []Partition{ + {}, {}, + }, + }, + }, + Filesystems: []Filesystem{ + { + Device: "/dev/sda", + }, + }, + }, + out: errors.ErrPartitionsOverwritten, + at: path.New("", "filesystems", 0, "device"), + }, + // test a disk with the same 'device' and 'WipeTable:=true' as a configured filesystem returns ErrFilesystemImplicitWipe + { + in: Storage{ + Disks: []Disk{ + { + Device: "/dev/sda", + WipeTable: util.BoolToPtr(true), + }, + }, + Filesystems: []Filesystem{ + { + Device: "/dev/sda", + }, + }, + }, + out: errors.ErrFilesystemImplicitWipe, + at: path.New("", "filesystems", 0, "device"), + }, + // test a disk with the same 'device' and 'WipeTable:=false' as a configured filesystem returns nil + { + in: Storage{ + Disks: []Disk{ + { + Device: "/dev/sda", + WipeTable: util.BoolToPtr(false), + }, + }, + Filesystems: []Filesystem{ + { + Device: "/dev/sda", + }, + }, + }, + out: nil, + }, + // test a disk with no partitions with the same 'device' as a filesystem returns nil + { + in: Storage{ + Disks: []Disk{ + { + Device: "/dev/sdb", + }, + }, + Filesystems: []Filesystem{ + { + Device: "/dev/sdb", + }, + }, + }, + out: nil, + }, + } + + for i, test := range tests { + r := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnWarn(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad report: want %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_6_experimental/types/systemd.go b/config/v3_6_experimental/types/systemd.go new file mode 100644 index 000000000..ac521ba73 --- /dev/null +++ b/config/v3_6_experimental/types/systemd.go @@ -0,0 +1,61 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "regexp" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/shared/parse" + "github.com/coreos/ignition/v2/config/util" + + vpath "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (s Systemd) Validate(c vpath.ContextPath) (r report.Report) { + units := make(map[string]Unit) + checkInstanceUnit := regexp.MustCompile(`^(.+?)@(.+?)\.service$`) + for _, d := range s.Units { + units[d.Name] = d + } + for index, unit := range s.Units { + if checkInstanceUnit.MatchString(unit.Name) && util.IsTrue(unit.Enabled) { + instUnitSlice := checkInstanceUnit.FindSubmatch([]byte(unit.Name)) + instantiableUnit := string(instUnitSlice[1]) + "@.service" + if _, ok := units[instantiableUnit]; ok && util.NotEmpty(units[instantiableUnit].Contents) { + foundInstallSection := false + // we're doing a separate validation pass on each unit to identify + // if an instantiable unit has the install section. So logging an + // `AddOnError` will produce duplicate errors on bad unit contents + // because we're already doing that while validating a unit separately. + opts, err := parse.ParseUnitContents(units[instantiableUnit].Contents) + if err != nil { + continue + } + for _, section := range opts { + if section.Section == "Install" { + foundInstallSection = true + break + } + } + if !foundInstallSection { + r.AddOnWarn(c.Append("units", index, "contents"), errors.NewNoInstallSectionForInstantiableUnitError(instantiableUnit, unit.Name)) + } + } + } + } + return +} diff --git a/config/v3_6_experimental/types/systemd_test.go b/config/v3_6_experimental/types/systemd_test.go new file mode 100644 index 000000000..798eb10f4 --- /dev/null +++ b/config/v3_6_experimental/types/systemd_test.go @@ -0,0 +1,105 @@ +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestSystemdValidate(t *testing.T) { + tests := []struct { + in Systemd + out error + }{ + { + Systemd{ + []Unit{ + {Name: "test@.service", Contents: util.StrToPtr("[Foo]\nQux=Bar")}, + {Name: "test@foo.service", Enabled: util.BoolToPtr(true)}, + }, + }, + errors.NewNoInstallSectionForInstantiableUnitError("test@.service", "test@foo.service"), + }, + { + Systemd{ + []Unit{ + {Name: "test2@.service", Contents: util.StrToPtr("[Foo]\nQux=Bar")}, + }, + }, + nil, + }, + { + Systemd{ + []Unit{ + {Name: "test@.service", Contents: util.StrToPtr("[Foo]\nQux=Bar")}, + {Name: "test@foo.service", Enabled: util.BoolToPtr(false)}, + }, + }, + nil, + }, + { + Systemd{ + []Unit{ + {Name: "test2@.service", Contents: util.StrToPtr("[Unit]\nDescription=echo service template\n[Service]\nType=oneshot\nExecStart=/bin/echo %i\n[Install]\nWantedBy=multi-user.target\n")}, + {Name: "test2@foo.service", Enabled: util.BoolToPtr(false)}, + }, + }, + nil, + }, + { + Systemd{ + []Unit{ + {Name: "test2@.service", Contents: util.StrToPtr("[Unit]\nDescription=echo service template\n[Service]\nType=oneshot\nExecStart=/bin/echo %i\n[Install]\nWantedBy=multi-user.target\n")}, + {Name: "test2@bar.service", Enabled: util.BoolToPtr(true)}, + }, + }, + nil, + }, + { + Systemd{ + []Unit{ + {Name: "test@.service", Contents: util.StrToPtr("[Unit]\nDescription=echo service template\n[Service]\nType=oneshot\nExecStart=/bin/echo %i\n[Install]\nWantedBy=multi-user.target\n")}, + {Name: "test2@foo.service", Enabled: util.BoolToPtr(true)}, + }, + }, + nil, + }, + { + Systemd{ + []Unit{ + {Name: "test@.service"}, + {Name: "test@bar.service", Enabled: util.BoolToPtr(true)}, + }, + }, + nil, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnWarn(path.ContextPath{}.Append("units", 1, "contents"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} diff --git a/config/v3_6_experimental/types/tang.go b/config/v3_6_experimental/types/tang.go new file mode 100644 index 000000000..1839d6cc3 --- /dev/null +++ b/config/v3_6_experimental/types/tang.go @@ -0,0 +1,65 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "encoding/json" + "net/url" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (t Tang) Key() string { + return t.URL +} + +func (t Tang) Validate(c path.ContextPath) (r report.Report) { + r.AddOnError(c.Append("url"), validateTangURL(t.URL)) + if util.NilOrEmpty(t.Thumbprint) { + r.AddOnError(c.Append("thumbprint"), errors.ErrTangThumbprintRequired) + } + r.AddOnError(c.Append("advertisement"), validateTangAdvertisement(t.Advertisement)) + return +} + +func validateTangURL(s string) error { + u, err := url.Parse(s) + if err != nil { + return errors.ErrInvalidUrl + } + + switch u.Scheme { + case "http", "https": + return nil + default: + return errors.ErrInvalidScheme + } +} + +func validateTangAdvertisement(s *string) error { + if util.NotEmpty(s) { + var adv any + err := json.Unmarshal([]byte(*s), &adv) + if err != nil { + return errors.ErrInvalidTangAdvertisement + } + } + + return nil +} diff --git a/config/v3_6_experimental/types/tang_test.go b/config/v3_6_experimental/types/tang_test.go new file mode 100644 index 000000000..fb57b1e4a --- /dev/null +++ b/config/v3_6_experimental/types/tang_test.go @@ -0,0 +1,104 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestTangValidation(t *testing.T) { + tests := []struct { + in Tang + out error + at path.ContextPath + }{ + // happy path with no advertisement and healthy url + { + in: Tang{ + URL: "http://example.com", + Thumbprint: util.StrToPtr("abc"), + }, + out: nil, + }, + // test 1: invalid url scheme + { + in: Tang{ + URL: "httasfdsafadsf", + Thumbprint: util.StrToPtr("abc"), + }, + out: errors.ErrInvalidScheme, + at: path.New("foo", "url"), + }, + // null url + { + in: Tang{ + Thumbprint: util.StrToPtr("abc"), + }, + out: errors.ErrInvalidScheme, + at: path.New("foo", "url"), + }, + // null thumbprint + { + in: Tang{ + URL: "http://example.com", + Thumbprint: nil, + }, + out: errors.ErrTangThumbprintRequired, + at: path.New("foo", "thumbprint"), + }, + // Advertisement is valid json + { + in: Tang{ + URL: "http://example.com", + Thumbprint: util.StrToPtr("abc"), + Advertisement: util.StrToPtr("{\"payload\": \"eyJrZXlzIjogW3siYWxnIjogIkVTNTEyIiwgImt0eSI6ICJFQyIsICJjcnYiOiAiUC01MjEiLCAieCI6ICJBRGFNajJmazNob21CWTF5WElSQ21uRk92cmUzOFZjdHMwTnNHeDZ6RWNxdEVXcjh5ekhUMkhfa2hjNGpSa19FQWFLdjNrd2RjZ05sOTBLcGhfMGYyQ190IiwgInkiOiAiQUZ2d1UyeGJ5T1RydWo0V1NtcVlqN2wtcUVTZmhWakdCNTI1Q2d6d0NoZUZRRTBvb1o3STYyamt3NkRKQ05yS3VPUDRsSEhicm8tYXhoUk9MSXNJVExvNCIsICJrZXlfb3BzIjogWyJ2ZXJpZnkiXX0sIHsiYWxnIjogIkVDTVIiLCAia3R5IjogIkVDIiwgImNydiI6ICJQLTUyMSIsICJ4IjogIkFOZDVYcTFvZklUbTdNWG16OUY0VVRSYmRNZFNIMl9XNXczTDVWZ0w3b3hwdmpyM0hkLXNLNUVqd3A1V2swMnJMb3NXVUJjYkZyZEhjZFJTTVJoZlVFTFIiLCAieSI6ICJBRVVaVlVZWkFBY2hVcmdoX3poaTV3SUUzeTEycGwzeWhqUk5LcGpSdW9tUFhKaDhRaFhXRmRWZEtMUlEwX1lwUjNOMjNSUk1pU1lvWlg0Qm42QnlrQVBMIiwgImtleV9vcHMiOiBbImRlcml2ZUtleSJdfV19\", \"protected\": \"eyJhbGciOiJFUzUxMiIsImN0eSI6Imp3ay1zZXQranNvbiJ9\", \"signature\": \"APHfSyVzLwELwG0pMJyIP74gWvhHUvDtv0SESBxA2uOdSXq76IdWHW2xvCZDdlNan8pnqUvEedPZjf_vdKBw9MTXAPMkRxVnu64HepKwlrzzm_zG2R4CHpoCOsGgjH9-acYxg-Vha63oMojv3_bV0VHg-NbzNLaxietgYplstvcNIwkv\"}"), + }, + out: nil, + }, + // Advertisement is empty string + { + in: Tang{ + URL: "http://example.com", + Thumbprint: util.StrToPtr("abc"), + Advertisement: util.StrToPtr(""), + }, + out: nil, + }, + // Advertisement is not valid json + { + in: Tang{ + URL: "http://example.com", + Thumbprint: util.StrToPtr("abc"), + Advertisement: util.StrToPtr("{{"), + }, + out: errors.ErrInvalidTangAdvertisement, + at: path.New("foo", "advertisement"), + }, + } + for i, test := range tests { + r := test.in.Validate(path.New("foo")) + expected := report.Report{} + expected.AddOnError(test.at, test.out) + if !reflect.DeepEqual(expected, r) { + t.Errorf("#%d: bad error: expected : %v, got %v", i, expected, r) + } + } +} diff --git a/config/v3_6_experimental/types/tls.go b/config/v3_6_experimental/types/tls.go new file mode 100644 index 000000000..8890e397e --- /dev/null +++ b/config/v3_6_experimental/types/tls.go @@ -0,0 +1,27 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (tls TLS) Validate(c path.ContextPath) (r report.Report) { + for i, ca := range tls.CertificateAuthorities { + r.AddOnError(c.Append("certificateAuthorities", i), ca.validateRequiredSource()) + } + return +} diff --git a/config/v3_6_experimental/types/tls_test.go b/config/v3_6_experimental/types/tls_test.go new file mode 100644 index 000000000..8f3b2e351 --- /dev/null +++ b/config/v3_6_experimental/types/tls_test.go @@ -0,0 +1,42 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/vcontext/validate" +) + +func TestTLSValidate(t *testing.T) { + tests := []struct { + in TLS + out string + }{ + { + TLS{ + CertificateAuthorities: []Resource{{}}, + }, + "error at $.certificateAuthorities.0: source is required\n", + }, + } + + for i, test := range tests { + r := validate.Validate(test.in, "test") + if test.out != r.String() { + t.Errorf("#%d: bad error: want %q, got %q", i, test.out, r.String()) + } + } +} diff --git a/config/v3_6_experimental/types/unit.go b/config/v3_6_experimental/types/unit.go new file mode 100644 index 000000000..c5ee1e8e3 --- /dev/null +++ b/config/v3_6_experimental/types/unit.go @@ -0,0 +1,68 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "path" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/shared/parse" + "github.com/coreos/ignition/v2/config/shared/validations" + "github.com/coreos/ignition/v2/config/util" + + cpath "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (u Unit) Key() string { + return u.Name +} + +func (d Dropin) Key() string { + return d.Name +} + +func (u Unit) Validate(c cpath.ContextPath) (r report.Report) { + r.AddOnError(c.Append("name"), validateName(u.Name)) + c = c.Append("contents") + opts, err := parse.ParseUnitContents(u.Contents) + r.AddOnError(c, err) + + r.AddOnWarn(c, validations.ValidateInstallSection(u.Name, util.IsTrue(u.Enabled), util.NilOrEmpty(u.Contents), opts)) + + return +} + +func validateName(name string) error { + switch path.Ext(name) { + case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope": + default: + return errors.ErrInvalidSystemdExt + } + return nil +} + +func (d Dropin) Validate(c cpath.ContextPath) (r report.Report) { + _, err := parse.ParseUnitContents(d.Contents) + r.AddOnError(c.Append("contents"), err) + + switch path.Ext(d.Name) { + case ".conf": + default: + r.AddOnError(c.Append("name"), errors.ErrInvalidSystemdDropinExt) + } + + return +} diff --git a/config/v3_6_experimental/types/unit_test.go b/config/v3_6_experimental/types/unit_test.go new file mode 100644 index 000000000..3dc905407 --- /dev/null +++ b/config/v3_6_experimental/types/unit_test.go @@ -0,0 +1,108 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "fmt" + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestSystemdUnitValidateContents(t *testing.T) { + tests := []struct { + in Unit + out error + }{ + { + Unit{Name: "test.service", Contents: util.StrToPtr("[Foo]\nQux=Bar")}, + nil, + }, + { + Unit{Name: "test.service", Contents: util.StrToPtr("[Foo")}, + fmt.Errorf("invalid unit content: unable to find end of section"), + }, + { + Unit{Name: "test.service", Contents: util.StrToPtr(""), Dropins: []Dropin{{}}}, + nil, + }, + } + + for i, test := range tests { + err := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(path.New("", "contents"), test.out) + if !reflect.DeepEqual(expected, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, expected, err) + } + } +} + +func TestSystemdUnitValidateName(t *testing.T) { + tests := []struct { + in string + out error + }{ + { + "test.service", + nil, + }, + { + "test.socket", + nil, + }, + { + "test.blah", + errors.ErrInvalidSystemdExt, + }, + } + + for i, test := range tests { + err := validateName(test.in) + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} + +func TestSystemdUnitDropInValidate(t *testing.T) { + tests := []struct { + in Dropin + out error + }{ + { + Dropin{Name: "test.conf", Contents: util.StrToPtr("[Foo]\nQux=Bar")}, + nil, + }, + { + Dropin{Name: "test.conf", Contents: util.StrToPtr("[Foo")}, + fmt.Errorf("invalid unit content: unable to find end of section"), + }, + } + + for i, test := range tests { + err := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(path.New("", "contents"), test.out) + if !reflect.DeepEqual(expected, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, expected, err) + } + } +} diff --git a/config/v3_6_experimental/types/url.go b/config/v3_6_experimental/types/url.go new file mode 100644 index 000000000..3ca189dae --- /dev/null +++ b/config/v3_6_experimental/types/url.go @@ -0,0 +1,83 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "net/url" + "strings" + + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/vincent-petithory/dataurl" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func validateURL(s string) error { + u, err := url.Parse(s) + if err != nil { + return errors.ErrInvalidUrl + } + + switch u.Scheme { + case "http", "https", "tftp", "gs": + return nil + case "s3": + if v, ok := u.Query()["versionId"]; ok { + if len(v) == 0 || v[0] == "" { + return errors.ErrInvalidS3ObjectVersionId + } + } + return nil + case "arn": + fullURL := u.Scheme + ":" + u.Opaque + if !arn.IsARN(fullURL) { + return errors.ErrInvalidS3ARN + } + s3arn, err := arn.Parse(fullURL) + if err != nil { + return err + } + if s3arn.Service != "s3" { + return errors.ErrInvalidS3ARN + } + urlSplit := strings.Split(fullURL, "/") + if strings.HasPrefix(s3arn.Resource, "accesspoint/") && len(urlSplit) < 3 { + return errors.ErrInvalidS3ARN + } else if len(urlSplit) < 2 { + return errors.ErrInvalidS3ARN + } + if v, ok := u.Query()["versionId"]; ok { + if len(v) == 0 || v[0] == "" { + return errors.ErrInvalidS3ObjectVersionId + } + } + return nil + case "data": + if _, err := dataurl.DecodeString(s); err != nil { + return err + } + return nil + default: + return errors.ErrInvalidScheme + } +} + +func validateURLNilOK(s *string) error { + if util.NilOrEmpty(s) { + return nil + } + return validateURL(*s) +} diff --git a/config/v3_6_experimental/types/url_test.go b/config/v3_6_experimental/types/url_test.go new file mode 100644 index 000000000..1901b7aff --- /dev/null +++ b/config/v3_6_experimental/types/url_test.go @@ -0,0 +1,137 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" +) + +func TestURLValidate(t *testing.T) { + tests := []struct { + in *string + out error + }{ + { + nil, + nil, + }, + { + util.StrToPtr(""), + nil, + }, + { + util.StrToPtr("http://example.com"), + nil, + }, + { + util.StrToPtr("https://example.com"), + nil, + }, + { + util.StrToPtr("tftp://example.com:69/foobar.txt"), + nil, + }, + { + util.StrToPtr("data:,example%20file%0A"), + nil, + }, + { + util.StrToPtr("bad://"), + errors.ErrInvalidScheme, + }, + { + util.StrToPtr("s3://bucket/key"), + nil, + }, + { + util.StrToPtr("s3://bucket/key?versionId="), + errors.ErrInvalidS3ObjectVersionId, + }, + { + util.StrToPtr("s3://bucket/key?versionId=aVersionHash"), + nil, + }, + { + util.StrToPtr("Arn:"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:iam:us-west-2:123456789012:resource"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:s3:::bucket-name-but-no-key"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint/"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint/accesspoint-name-but-no-bucket"), + errors.ErrInvalidS3ARN, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:bucket-name/object-key"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:bucket-name/object-key?versionId=aVersionHash"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint/accesspoint-name/object"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint/accesspoint-name/some/nested/object"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:us-west-2:123456789012:accesspoint/accesspoint-name/object?versionId=aVersionHash"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:::bucket-name/object-key"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:::bucket-name/some/nested/object"), + nil, + }, + { + util.StrToPtr("arn:aws:s3:::bucket-name/object-key?versionId=aVersionHash"), + nil, + }, + { + util.StrToPtr("gs://bucket/object"), + nil, + }, + } + + for i, test := range tests { + err := validateURLNilOK(test.in) + if test.out != err { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, err) + } + } +} diff --git a/config/v3_6_experimental/types/verification.go b/config/v3_6_experimental/types/verification.go new file mode 100644 index 000000000..5def6f04b --- /dev/null +++ b/config/v3_6_experimental/types/verification.go @@ -0,0 +1,71 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "crypto" + "encoding/hex" + "strings" + + "github.com/coreos/ignition/v2/config/shared/errors" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// 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 "", "", errors.ErrHashMalformed + } + + return parts[0], parts[1], nil +} + +func (v Verification) Validate(c path.ContextPath) (r report.Report) { + c = c.Append("hash") + if v.Hash == nil { + // The hash can be nil + return + } + + function, sum, err := v.HashParts() + if err != nil { + r.AddOnError(c, err) + return + } + var hash crypto.Hash + switch function { + case "sha512": + hash = crypto.SHA512 + case "sha256": + hash = crypto.SHA256 + default: + r.AddOnError(c, errors.ErrHashUnrecognized) + return + } + + if len(sum) != hex.EncodedLen(hash.Size()) { + r.AddOnError(c, errors.ErrHashWrongSize) + } + + return +} diff --git a/config/v3_6_experimental/types/verification_test.go b/config/v3_6_experimental/types/verification_test.go new file mode 100644 index 000000000..9a0914d08 --- /dev/null +++ b/config/v3_6_experimental/types/verification_test.go @@ -0,0 +1,102 @@ +// Copyright 2020 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "reflect" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func TestHashParts(t *testing.T) { + tests := []struct { + in string + out error + }{ + { + `"sha512-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"`, + nil, + }, + { + `"sha256-0519a9826023338828942b081814355d55301b9bc82042390f9afaf75cd3a707"`, + nil, + }, + { + `"sha512:01234567"`, + errors.ErrHashMalformed, + }, + { + `"sha256:12345678"`, + errors.ErrHashMalformed, + }, + } + + for i, test := range tests { + fun, sum, err := Verification{Hash: &test.in}.HashParts() + if err != test.out { + t.Fatalf("#%d: bad error: want %+v, got %+v", i, test.out, err) + } + if err == nil && fun+"-"+sum != test.in { + t.Fatalf("#%d: bad hash: want %+v, got %+v", i, test.in, fun+"-"+sum) + } + } +} + +func TestHashValidate(t *testing.T) { + h1 := "xor-abcdef" + h2 := "sha512-123" + h3 := "sha512-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + h4 := "sha256-0519a9826023338828942b081814355d55301b9bc82042390f9afaf75cd3a707" + h5 := "sha256-345" + + tests := []struct { + in Verification + out error + }{ + { + Verification{Hash: &h1}, + errors.ErrHashUnrecognized, + }, + { + Verification{Hash: &h2}, + errors.ErrHashWrongSize, + }, + { + Verification{Hash: &h3}, + nil, + }, + { + Verification{Hash: &h4}, + nil, + }, + { + Verification{Hash: &h5}, + errors.ErrHashWrongSize, + }, + } + + for i, test := range tests { + err := test.in.Validate(path.ContextPath{}) + expected := report.Report{} + expected.AddOnError(path.New("", "hash"), test.out) + if !reflect.DeepEqual(expected, err) { + t.Errorf("#%d: bad error: want %v, got %v", i, expected, err) + } + } +} diff --git a/docs/configuration-v3_5.md b/docs/configuration-v3_5.md new file mode 100644 index 000000000..08f6336ee --- /dev/null +++ b/docs/configuration-v3_5.md @@ -0,0 +1,190 @@ +--- +# This file is automatically generated from config/doc and internal/doc. +# Do not edit. +title: Config Spec v3.5.0 +parent: Configuration specifications +nav_order: 44 +--- + +# Configuration Specification v3.5.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 (`3.5.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_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **source** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_httpHeaders_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `