diff --git a/config/config.go b/config/config.go index b4c22c49..93aa2a03 100644 --- a/config/config.go +++ b/config/config.go @@ -38,8 +38,9 @@ import ( openshift4_14 "github.com/coreos/butane/config/openshift/v4_14" openshift4_15 "github.com/coreos/butane/config/openshift/v4_15" openshift4_16 "github.com/coreos/butane/config/openshift/v4_16" - openshift4_17_exp "github.com/coreos/butane/config/openshift/v4_17" - openshift4_18_exp "github.com/coreos/butane/config/openshift/v4_18_exp" + openshift4_17 "github.com/coreos/butane/config/openshift/v4_17" + openshift4_18 "github.com/coreos/butane/config/openshift/v4_18" + openshift4_19_exp "github.com/coreos/butane/config/openshift/v4_19_exp" openshift4_8 "github.com/coreos/butane/config/openshift/v4_8" openshift4_9 "github.com/coreos/butane/config/openshift/v4_9" r4e1_0 "github.com/coreos/butane/config/r4e/v1_0" @@ -82,8 +83,9 @@ func init() { RegisterTranslator("openshift", "4.14.0", openshift4_14.ToConfigBytes) RegisterTranslator("openshift", "4.15.0", openshift4_15.ToConfigBytes) RegisterTranslator("openshift", "4.16.0", openshift4_16.ToConfigBytes) - RegisterTranslator("openshift", "4.17.0", openshift4_17_exp.ToConfigBytes) - RegisterTranslator("openshift", "4.18.0-experimental", openshift4_18_exp.ToConfigBytes) + RegisterTranslator("openshift", "4.17.0", openshift4_17.ToConfigBytes) + RegisterTranslator("openshift", "4.18.0", openshift4_18.ToConfigBytes) + RegisterTranslator("openshift", "4.19.0-experimental", openshift4_19_exp.ToConfigBytes) RegisterTranslator("r4e", "1.0.0", r4e1_0.ToIgn3_3Bytes) RegisterTranslator("r4e", "1.1.0", r4e1_1.ToIgn3_4Bytes) RegisterTranslator("r4e", "1.2.0-experimental", r4e1_2_exp.ToIgn3_6Bytes) diff --git a/config/openshift/v4_18/result/schema.go b/config/openshift/v4_18/result/schema.go new file mode 100644 index 00000000..ad5abd8e --- /dev/null +++ b/config/openshift/v4_18/result/schema.go @@ -0,0 +1,48 @@ +// 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 result + +import ( + "github.com/coreos/ignition/v2/config/v3_4/types" +) + +const ( + MC_API_VERSION = "machineconfiguration.openshift.io/v1" + MC_KIND = "MachineConfig" +) + +// We round-trip through JSON because Ignition uses `json` struct tags, +// so all struct tags need to be `json` even though we're ultimately +// writing YAML. + +type MachineConfig struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` +} + +type Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels,omitempty"` +} + +type Spec struct { + Config types.Config `json:"config"` + KernelArguments []string `json:"kernelArguments,omitempty"` + Extensions []string `json:"extensions,omitempty"` + FIPS *bool `json:"fips,omitempty"` + KernelType *string `json:"kernelType,omitempty"` +} diff --git a/config/openshift/v4_18/schema.go b/config/openshift/v4_18/schema.go new file mode 100644 index 00000000..c9da45ad --- /dev/null +++ b/config/openshift/v4_18/schema.go @@ -0,0 +1,39 @@ +// 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 v4_18 + +import ( + fcos "github.com/coreos/butane/config/fcos/v1_5" +) + +const ROLE_LABEL_KEY = "machineconfiguration.openshift.io/role" + +type Config struct { + fcos.Config `yaml:",inline"` + Metadata Metadata `yaml:"metadata"` + OpenShift OpenShift `yaml:"openshift"` +} + +type Metadata struct { + Name string `yaml:"name"` + Labels map[string]string `yaml:"labels,omitempty"` +} + +type OpenShift struct { + KernelArguments []string `yaml:"kernel_arguments"` + Extensions []string `yaml:"extensions"` + FIPS *bool `yaml:"fips"` + KernelType *string `yaml:"kernel_type"` +} diff --git a/config/openshift/v4_18/translate.go b/config/openshift/v4_18/translate.go new file mode 100644 index 00000000..b3d78f74 --- /dev/null +++ b/config/openshift/v4_18/translate.go @@ -0,0 +1,303 @@ +// 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 v4_18 + +import ( + "net/url" + "strings" + + "github.com/coreos/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_18/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_18Unvalidated translates the config to a MachineConfig. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToMachineConfig4_18Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_4Unvalidated(options) + if r.IsFatal() { + return result.MachineConfig{}, ts, r + } + + // wrap + ts = ts.PrefixPaths(path.New("yaml"), path.New("json", "spec", "config")) + mc := result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: c.Metadata.Name, + Labels: make(map[string]string), + }, + Spec: result.Spec{ + Config: cfg, + }, + } + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "apiVersion")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "kind")) + ts.AddTranslation(path.New("yaml", "metadata"), path.New("json", "metadata")) + ts.AddTranslation(path.New("yaml", "metadata", "name"), path.New("json", "metadata", "name")) + ts.AddTranslation(path.New("yaml", "metadata", "labels"), path.New("json", "metadata", "labels")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "spec")) + ts.AddTranslation(path.New("yaml"), path.New("json", "spec", "config")) + for k, v := range c.Metadata.Labels { + mc.Metadata.Labels[k] = v + ts.AddTranslation(path.New("yaml", "metadata", "labels", k), path.New("json", "metadata", "labels", k)) + } + + // translate OpenShift fields + tr := translate.NewTranslator("yaml", "json", options) + from := &c.OpenShift + to := &mc.Spec + ts2, r2 := translate.Prefixed(tr, "extensions", &from.Extensions, &to.Extensions) + translate.MergeP(tr, ts2, &r2, "fips", &from.FIPS, &to.FIPS) + translate.MergeP2(tr, ts2, &r2, "kernel_arguments", &from.KernelArguments, "kernelArguments", &to.KernelArguments) + translate.MergeP2(tr, ts2, &r2, "kernel_type", &from.KernelType, "kernelType", &to.KernelType) + ts.MergeP2("openshift", "spec", ts2) + r.Merge(r2) + + // apply FIPS options to LUKS volumes + ts.Merge(addLuksFipsOptions(&mc)) + + // finally, check the fully desugared config for RHCOS and MCO support + r.Merge(validateRHCOSSupport(mc)) + r.Merge(validateMCOSupport(mc)) + + return mc, ts, r +} + +// ToMachineConfig4_18 translates the config to a MachineConfig. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToMachineConfig4_18(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_18Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_4Unvalidated translates the config to an Ignition config. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToIgn3_4Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_18Unvalidated(options) + cfg := mc.Spec.Config + + // report warnings if there are any non-empty fields in Spec (other + // than the Ignition config itself) that we're ignoring + mc.Spec.Config = types.Config{} + warnings := translate.PrefixReport(cutil.CheckForElidedFields(mc.Spec), "spec") + // translate from json space into yaml space, since the caller won't + // have enough info to do it + r.Merge(cutil.TranslateReportPaths(warnings, ts)) + + ts = ts.Descend(path.New("json", "spec", "config")) + return cfg, ts, r +} + +// ToIgn3_4 translates the config to an Ignition config. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToIgn3_4(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_4Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.17 Butane config to a v4.17 MachineConfig or a v3.6.0-experimental Ignition config. It returns a report of any errors or +// warnings in the source and resultant config. If the report has fatal errors or it encounters other problems +// translating, an error is returned. +func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + if options.Raw { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_18", options) + } +} + +func addLuksFipsOptions(mc *result.MachineConfig) translate.TranslationSet { + ts := translate.NewTranslationSet("yaml", "json") + if !util.IsTrue(mc.Spec.FIPS) { + return ts + } + +OUTER: + for i := range mc.Spec.Config.Storage.Luks { + luks := &mc.Spec.Config.Storage.Luks[i] + // Only add options if the user hasn't already specified + // a cipher option. Do this in-place, since config merging + // doesn't support conditional logic. + for _, option := range luks.Options { + if option == fipsCipherOption || + strings.HasPrefix(string(option), string(fipsCipherOption)+"=") || + option == fipsCipherShortOption { + continue OUTER + } + } + for j := 0; j < 2; j++ { + ts.AddTranslation(path.New("yaml", "openshift", "fips"), path.New("json", "spec", "config", "storage", "luks", i, "options", len(luks.Options)+j)) + } + if len(luks.Options) == 0 { + ts.AddTranslation(path.New("yaml", "openshift", "fips"), path.New("json", "spec", "config", "storage", "luks", i, "options")) + } + luks.Options = append(luks.Options, fipsCipherOption, fipsCipherArgument) + } + return ts +} + +// Error on fields that are rejected by RHCOS. +// +// Some of these fields may have been generated by sugar (e.g. +// boot_device.luks), so we work in JSON (output) space and then translate +// paths back to YAML (input) space. That's also the reason we do these +// checks after translation, rather than during validation. +func validateRHCOSSupport(mc result.MachineConfig) report.Report { + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "btrfs" { + // we don't ship mkfs.btrfs + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrBtrfsSupport) + } + } + return r +} + +// Error on fields that are rejected outright by the MCO, or that are +// unsupported by the MCO and we want to discourage. +// +// https://github.com/openshift/machine-config-operator/blob/d6dabadeca05/MachineConfigDaemon.md#supported-vs-unsupported-ignition-config-changes +// +// Some of these fields may have been generated by sugar (e.g. storage.trees), +// so we work in JSON (output) space and then translate paths back to YAML +// (input) space. That's also the reason we do these checks after +// translation, rather than during validation. +func validateMCOSupport(mc result.MachineConfig) report.Report { + // See also fieldFilters at the top of this file. + + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "none" { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrFilesystemNoneSupport) + } + } + for i, file := range mc.Spec.Config.Storage.Files { + if file.Contents.Source != nil { + fileSource, err := url.Parse(*file.Contents.Source) + // parse errors will be caught by normal config validation + if err == nil && fileSource.Scheme != "data" { + // FORBIDDEN + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "contents", "source"), common.ErrFileSchemeSupport) + } + } + if file.Mode != nil && *file.Mode & ^0777 != 0 { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "mode"), common.ErrFileSpecialModeSupport) + } + } + for i, user := range mc.Spec.Config.Passwd.Users { + if user.Name != "core" { + // TRIPWIRE + r.AddOnError(path.New("json", "spec", "config", "passwd", "users", i, "name"), common.ErrUserNameSupport) + } + } + return r +} diff --git a/config/openshift/v4_18/translate_test.go b/config/openshift/v4_18/translate_test.go new file mode 100644 index 00000000..117f8965 --- /dev/null +++ b/config/openshift/v4_18/translate_test.go @@ -0,0 +1,515 @@ +// 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 v4_18 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_18/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// Test post-translation validation of RHCOS/MCO support for Ignition config fields. +func TestValidateSupport(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + []entry{}, + }, + // core user with only accepted fields + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + PasswordHash: util.StrToPtr("corned beef"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // valid data URL + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + Contents: base.Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // all the warnings/errors + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + }, + { + Path: "/g", + Append: []base.Resource{ + { + Inline: util.StrToPtr("z"), + }, + }, + }, + { + Path: "/h", + Contents: base.Resource{ + Source: util.StrToPtr("https://example.com/"), + }, + Mode: util.IntToPtr(04755), + }, + { + Path: "/i", + Contents: base.Resource{ + Source: util.StrToPtr("data:,z"), + HTTPHeaders: base.HTTPHeaders{ + { + Name: "foo", + Value: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/vda4", + Format: util.StrToPtr("btrfs"), + }, + { + Device: "/dev/vda5", + Format: util.StrToPtr("none"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: util.StrToPtr("/t"), + }, + }, + }, + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + Gecos: util.StrToPtr("mercury delay line"), + Groups: []base.Group{ + "z", + }, + HomeDir: util.StrToPtr("/home/drum"), + NoCreateHome: util.BoolToPtr(true), + NoLogInit: util.BoolToPtr(true), + NoUserGroup: util.BoolToPtr(true), + PasswordHash: util.StrToPtr("corned beef"), + PrimaryGroup: util.StrToPtr("wheel"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + SSHAuthorizedKeysLocal: []string{}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "foo", + }, + ShouldNotExist: []base.KernelArgument{ + "bar", + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "format")}, + {report.Error, common.ErrFilesystemNoneSupport, path.New("yaml", "storage", "filesystems", 1, "format")}, + {report.Error, common.ErrFileSchemeSupport, path.New("yaml", "storage", "files", 2, "contents", "source")}, + {report.Error, common.ErrFileSpecialModeSupport, path.New("yaml", "storage", "files", 2, "mode")}, + {report.Error, common.ErrUserNameSupport, path.New("yaml", "passwd", "users", 1, "name")}, + // filters + {report.Error, common.ErrKernelArgumentSupport, path.New("yaml", "kernel_arguments")}, + {report.Error, common.ErrGroupSupport, path.New("yaml", "passwd", "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "gecos")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "home_dir")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_create_home")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_log_init")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_user_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "primary_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "shell")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "should_exist")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "system")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "uid")}, + {report.Error, common.ErrDirectorySupport, path.New("yaml", "storage", "directories")}, + {report.Error, common.ErrFileAppendSupport, path.New("yaml", "storage", "files", 1, "append")}, + {report.Error, common.ErrFileHeaderSupport, path.New("yaml", "storage", "files", 3, "contents", "http_headers")}, + {report.Error, common.ErrLinkSupport, path.New("yaml", "storage", "links")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + var expectedReport report.Report + for _, entry := range test.entries { + expectedReport.AddOn(entry.path, entry.err, entry.kind) + } + actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + r.Merge(fieldFilters.Verify(actual)) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expectedReport, r, "report mismatch") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/config/openshift/v4_18_exp/validate.go b/config/openshift/v4_18/validate.go similarity index 98% rename from config/openshift/v4_18_exp/validate.go rename to config/openshift/v4_18/validate.go index e8e744d0..a45ec606 100644 --- a/config/openshift/v4_18_exp/validate.go +++ b/config/openshift/v4_18/validate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_18_exp +package v4_18 import ( "github.com/coreos/butane/config/common" diff --git a/config/openshift/v4_18_exp/validate_test.go b/config/openshift/v4_18/validate_test.go similarity index 99% rename from config/openshift/v4_18_exp/validate_test.go rename to config/openshift/v4_18/validate_test.go index 1fdd1242..22059e21 100644 --- a/config/openshift/v4_18_exp/validate_test.go +++ b/config/openshift/v4_18/validate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_18_exp +package v4_18 import ( "fmt" diff --git a/config/openshift/v4_18_exp/result/schema.go b/config/openshift/v4_19_exp/result/schema.go similarity index 100% rename from config/openshift/v4_18_exp/result/schema.go rename to config/openshift/v4_19_exp/result/schema.go diff --git a/config/openshift/v4_18_exp/schema.go b/config/openshift/v4_19_exp/schema.go similarity index 98% rename from config/openshift/v4_18_exp/schema.go rename to config/openshift/v4_19_exp/schema.go index 36df2720..5acdd04a 100644 --- a/config/openshift/v4_18_exp/schema.go +++ b/config/openshift/v4_19_exp/schema.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_18_exp +package v4_19_exp import ( fcos "github.com/coreos/butane/config/fcos/v1_7_exp" diff --git a/config/openshift/v4_18_exp/translate.go b/config/openshift/v4_19_exp/translate.go similarity index 97% rename from config/openshift/v4_18_exp/translate.go rename to config/openshift/v4_19_exp/translate.go index a74f793a..e535e612 100644 --- a/config/openshift/v4_18_exp/translate.go +++ b/config/openshift/v4_19_exp/translate.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_18_exp +package v4_19_exp import ( "net/url" "strings" "github.com/coreos/butane/config/common" - "github.com/coreos/butane/config/openshift/v4_18_exp/result" + "github.com/coreos/butane/config/openshift/v4_19_exp/result" cutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" @@ -112,7 +112,7 @@ func (c Config) FieldFilters() *cutil.FieldFilters { // returns the set of translations it did so paths in the resultant config // can be tracked back to their source in the source config. No config // validation is performed on input or output. -func (c Config) ToMachineConfig4_18Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { +func (c Config) ToMachineConfig4_19Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { cfg, ts, r := c.Config.ToIgn3_6Unvalidated(options) if r.IsFatal() { return result.MachineConfig{}, ts, r @@ -169,8 +169,8 @@ func (c Config) ToMachineConfig4_18Unvalidated(options common.TranslateOptions) // report of any errors or warnings in the source and resultant config. If // the report has fatal errors or it encounters other problems translating, // an error is returned. -func (c Config) ToMachineConfig4_18(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToMachineConfig4_18Unvalidated", options) +func (c Config) ToMachineConfig4_19(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_19Unvalidated", options) return cfg.(result.MachineConfig), r, err } @@ -179,7 +179,7 @@ func (c Config) ToMachineConfig4_18(options common.TranslateOptions) (result.Mac // can be tracked back to their source in the source config. No config // validation is performed on input or output. func (c Config) ToIgn3_6Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { - mc, ts, r := c.ToMachineConfig4_18Unvalidated(options) + mc, ts, r := c.ToMachineConfig4_19Unvalidated(options) cfg := mc.Spec.Config // report warnings if there are any non-empty fields in Spec (other @@ -210,7 +210,7 @@ func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, if options.Raw { return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) } else { - return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_18", options) + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_19", options) } } diff --git a/config/openshift/v4_18_exp/translate_test.go b/config/openshift/v4_19_exp/translate_test.go similarity index 99% rename from config/openshift/v4_18_exp/translate_test.go rename to config/openshift/v4_19_exp/translate_test.go index 7b8a0f8b..57a6cd91 100644 --- a/config/openshift/v4_18_exp/translate_test.go +++ b/config/openshift/v4_19_exp/translate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_18_exp +package v4_19_exp import ( "fmt" @@ -22,7 +22,7 @@ import ( base "github.com/coreos/butane/base/v0_7_exp" "github.com/coreos/butane/config/common" fcos "github.com/coreos/butane/config/fcos/v1_7_exp" - "github.com/coreos/butane/config/openshift/v4_18_exp/result" + "github.com/coreos/butane/config/openshift/v4_19_exp/result" confutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" @@ -359,7 +359,7 @@ func TestTranslateConfig(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { - actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.in.ToMachineConfig4_19Unvalidated(common.TranslateOptions{}) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.in, r) assert.Equal(t, test.out, actual, "translation mismatch") @@ -587,7 +587,7 @@ func TestValidateSupport(t *testing.T) { for _, entry := range test.entries { expectedReport.AddOn(entry.path, entry.err, entry.kind) } - actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.in.ToMachineConfig4_19Unvalidated(common.TranslateOptions{}) r.Merge(fieldFilters.Verify(actual)) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.in, r) diff --git a/config/openshift/v4_19_exp/validate.go b/config/openshift/v4_19_exp/validate.go new file mode 100644 index 00000000..969bf98d --- /dev/null +++ b/config/openshift/v4_19_exp/validate.go @@ -0,0 +1,43 @@ +// 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 v4_19_exp + +import ( + "github.com/coreos/butane/config/common" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (m Metadata) Validate(c path.ContextPath) (r report.Report) { + if m.Name == "" { + r.AddOnError(c.Append("name"), common.ErrNameRequired) + } + if m.Labels[ROLE_LABEL_KEY] == "" { + r.AddOnError(c.Append("labels"), common.ErrRoleRequired) + } + return +} + +func (os OpenShift) Validate(c path.ContextPath) (r report.Report) { + if os.KernelType != nil { + switch *os.KernelType { + case "", "default", "realtime": + default: + r.AddOnError(c.Append("kernel_type"), common.ErrInvalidKernelType) + } + } + return +} diff --git a/config/openshift/v4_19_exp/validate_test.go b/config/openshift/v4_19_exp/validate_test.go new file mode 100644 index 00000000..fcebdd08 --- /dev/null +++ b/config/openshift/v4_19_exp/validate_test.go @@ -0,0 +1,254 @@ +// 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 v4_19_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "Unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/docs/config-openshift-v4_18.md b/docs/config-openshift-v4_18.md new file mode 100644 index 00000000..f793c344 --- /dev/null +++ b/docs/config-openshift-v4_18.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.18.0 +parent: Configuration specifications +nav_order: 139 +--- + +# OpenShift Specification v4.18.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.18.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `