diff --git a/config/config.go b/config/config.go index de06b757..dc34dbe3 100644 --- a/config/config.go +++ b/config/config.go @@ -44,7 +44,8 @@ import ( openshift4_19 "github.com/coreos/butane/config/openshift/v4_19" openshift4_20 "github.com/coreos/butane/config/openshift/v4_20" openshift4_21 "github.com/coreos/butane/config/openshift/v4_21" - openshift4_22_exp "github.com/coreos/butane/config/openshift/v4_22_exp" + openshift4_22 "github.com/coreos/butane/config/openshift/v4_22" + openshift4_23_exp "github.com/coreos/butane/config/openshift/v4_23_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" @@ -93,7 +94,8 @@ func init() { RegisterTranslator("openshift", "4.19.0", openshift4_19.ToConfigBytes) RegisterTranslator("openshift", "4.20.0", openshift4_20.ToConfigBytes) RegisterTranslator("openshift", "4.21.0", openshift4_21.ToConfigBytes) - RegisterTranslator("openshift", "4.22.0-experimental", openshift4_22_exp.ToConfigBytes) + RegisterTranslator("openshift", "4.22.0", openshift4_22.ToConfigBytes) + RegisterTranslator("openshift", "4.23.0-experimental", openshift4_23_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_7Bytes) diff --git a/config/openshift/v4_22/result/schema.go b/config/openshift/v4_22/result/schema.go new file mode 100644 index 00000000..c38ee02e --- /dev/null +++ b/config/openshift/v4_22/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_6/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_22/schema.go b/config/openshift/v4_22/schema.go new file mode 100644 index 00000000..8f76b49e --- /dev/null +++ b/config/openshift/v4_22/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_22 + +import ( + fcos "github.com/coreos/butane/config/fcos/v1_7" +) + +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_22/translate.go b/config/openshift/v4_22/translate.go new file mode 100644 index 00000000..f4b76841 --- /dev/null +++ b/config/openshift/v4_22/translate.go @@ -0,0 +1,261 @@ +// 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_22 + +import ( + "net/url" + + "github.com/coreos/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_22/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_6/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. + +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_22Unvalidated 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_22Unvalidated(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 + } + + // 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) + + // finally, check the fully desugared config for RHCOS and MCO support + r.Merge(validateRHCOSSupport(mc)) + r.Merge(validateMCOSupport(mc)) + + return mc, ts, r +} + +// ToMachineConfig4_22 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_22(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_22Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_6Unvalidated translates the config to an Ignition config. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToIgn3_6Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_22Unvalidated(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_6 translates the config to an Ignition config. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.22 Butane config to a v4.22 MachineConfig or a v3.6.0 Ignition config. It returns a report of any errors or +// warnings in the source and resultant config. If the report has fatal errors or it encounters other problems +// translating, an error is returned. +func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + if options.Raw { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_22", options) + } +} + +// 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_22/translate_test.go b/config/openshift/v4_22/translate_test.go new file mode 100644 index 00000000..9527cf0e --- /dev/null +++ b/config/openshift/v4_22/translate_test.go @@ -0,0 +1,341 @@ +// 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_22 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_7" + "github.com/coreos/butane/config/openshift/v4_22/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/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_6Unvalidated(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.6.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")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_22Unvalidated(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_22Unvalidated(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_22_exp/validate.go b/config/openshift/v4_22/validate.go similarity index 99% rename from config/openshift/v4_22_exp/validate.go rename to config/openshift/v4_22/validate.go index 59751441..2de8b2aa 100644 --- a/config/openshift/v4_22_exp/validate.go +++ b/config/openshift/v4_22/validate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_22_exp +package v4_22 import ( "slices" diff --git a/config/openshift/v4_22/validate_test.go b/config/openshift/v4_22/validate_test.go new file mode 100644 index 00000000..33fd75c9 --- /dev/null +++ b/config/openshift/v4_22/validate_test.go @@ -0,0 +1,388 @@ +// 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_22 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_7" + + "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") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // missing kargs for CEX support + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-virt"), + Luks: fcos.BootDeviceLuks{ + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + { + Device: "/dev/mapper/foo", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("foo"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []base.Luks{ + { + Name: "root", + Label: util.StrToPtr("luks-root"), + Device: util.StrToPtr("/dev/disk/by-label/root"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + { + Name: "foo", + Label: util.StrToPtr("luks-foo"), + Device: util.StrToPtr("/dev/disk/by-label/foo"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + } + + 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/config/openshift/v4_22_exp/result/schema.go b/config/openshift/v4_23_exp/result/schema.go similarity index 100% rename from config/openshift/v4_22_exp/result/schema.go rename to config/openshift/v4_23_exp/result/schema.go diff --git a/config/openshift/v4_22_exp/schema.go b/config/openshift/v4_23_exp/schema.go similarity index 98% rename from config/openshift/v4_22_exp/schema.go rename to config/openshift/v4_23_exp/schema.go index bc94cd92..aac39e80 100644 --- a/config/openshift/v4_22_exp/schema.go +++ b/config/openshift/v4_23_exp/schema.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_22_exp +package v4_23_exp import ( fcos "github.com/coreos/butane/config/fcos/v1_8_exp" diff --git a/config/openshift/v4_22_exp/translate.go b/config/openshift/v4_23_exp/translate.go similarity index 95% rename from config/openshift/v4_22_exp/translate.go rename to config/openshift/v4_23_exp/translate.go index 15100f0c..f76aefb0 100644 --- a/config/openshift/v4_22_exp/translate.go +++ b/config/openshift/v4_23_exp/translate.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_22_exp +package v4_23_exp import ( "net/url" "github.com/coreos/butane/config/common" - "github.com/coreos/butane/config/openshift/v4_22_exp/result" + "github.com/coreos/butane/config/openshift/v4_23_exp/result" cutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" @@ -99,11 +99,11 @@ func (c Config) FieldFilters() *cutil.FieldFilters { return &fieldFilters } -// ToMachineConfig4_22Unvalidated translates the config to a MachineConfig. It also +// ToMachineConfig4_23Unvalidated 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_22Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { +func (c Config) ToMachineConfig4_23Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { cfg, ts, r := c.Config.ToIgn3_7Unvalidated(options) if r.IsFatal() { return result.MachineConfig{}, ts, r @@ -153,12 +153,12 @@ func (c Config) ToMachineConfig4_22Unvalidated(options common.TranslateOptions) return mc, ts, r } -// ToMachineConfig4_22 translates the config to a MachineConfig. It returns a +// ToMachineConfig4_23 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_22(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { - cfg, r, err := cutil.Translate(c, "ToMachineConfig4_22Unvalidated", options) +func (c Config) ToMachineConfig4_23(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_23Unvalidated", options) return cfg.(result.MachineConfig), r, err } @@ -167,7 +167,7 @@ func (c Config) ToMachineConfig4_22(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_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { - mc, ts, r := c.ToMachineConfig4_22Unvalidated(options) + mc, ts, r := c.ToMachineConfig4_23Unvalidated(options) cfg := mc.Spec.Config // report warnings if there are any non-empty fields in Spec (other @@ -191,14 +191,14 @@ func (c Config) ToIgn3_7(options common.TranslateOptions) (types.Config, report. return cfg.(types.Config), r, err } -// ToConfigBytes translates from a v4.22 Butane config to a v4.22 MachineConfig or a v3.6.0-experimental Ignition config. It returns a report of any errors or +// ToConfigBytes translates from a v4.23 Butane config to a v4.23 MachineConfig or a v3.7.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_7", options) } else { - return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_22", options) + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_23", options) } } diff --git a/config/openshift/v4_22_exp/translate_test.go b/config/openshift/v4_23_exp/translate_test.go similarity index 98% rename from config/openshift/v4_22_exp/translate_test.go rename to config/openshift/v4_23_exp/translate_test.go index d0f709d9..e7be1d06 100644 --- a/config/openshift/v4_22_exp/translate_test.go +++ b/config/openshift/v4_23_exp/translate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_22_exp +package v4_23_exp import ( "fmt" @@ -22,7 +22,7 @@ import ( base "github.com/coreos/butane/base/v0_8_exp" "github.com/coreos/butane/config/common" fcos "github.com/coreos/butane/config/fcos/v1_8_exp" - "github.com/coreos/butane/config/openshift/v4_22_exp/result" + "github.com/coreos/butane/config/openshift/v4_23_exp/result" confutil "github.com/coreos/butane/config/util" "github.com/coreos/butane/translate" @@ -185,7 +185,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_22Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.in.ToMachineConfig4_23Unvalidated(common.TranslateOptions{}) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.in, r) assert.Equal(t, test.out, actual, "translation mismatch") @@ -413,7 +413,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_22Unvalidated(common.TranslateOptions{}) + actual, translations, r := test.in.ToMachineConfig4_23Unvalidated(common.TranslateOptions{}) r.Merge(fieldFilters.Verify(actual)) r = confutil.TranslateReportPaths(r, translations) baseutil.VerifyReport(t, test.in, r) diff --git a/config/openshift/v4_23_exp/validate.go b/config/openshift/v4_23_exp/validate.go new file mode 100644 index 00000000..0a081ee1 --- /dev/null +++ b/config/openshift/v4_23_exp/validate.go @@ -0,0 +1,68 @@ +// 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_23_exp + +import ( + "slices" + + "github.com/coreos/butane/config/common" + "github.com/coreos/ignition/v2/config/util" + + "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 +} + +// Validate that we have the required kernel argument pointing to the key file +// if we have CEX support enabled. We only do this in the openshift spec as +// this is implemented differently in the fcos one. +// See: https://github.com/coreos/butane/issues/611 +// See: https://github.com/coreos/butane/issues/613 +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + if util.IsTrue(conf.BootDevice.Luks.Cex.Enabled) && !slices.Contains(conf.OpenShift.KernelArguments, "rd.luks.key=/etc/luks/cex.key") { + r.AddOnError(c.Append("openshift", "kernel_arguments"), common.ErrMissingKernelArgumentCex) + } + cex := false + for _, l := range conf.Storage.Luks { + if util.IsTrue(l.Cex.Enabled) && l.Name == "root" { + cex = true + } + } + if cex && !slices.Contains(conf.OpenShift.KernelArguments, "rd.luks.key=/etc/luks/cex.key") { + r.AddOnError(c.Append("openshift", "kernel_arguments"), common.ErrMissingKernelArgumentCex) + } + + return +} diff --git a/config/openshift/v4_22_exp/validate_test.go b/config/openshift/v4_23_exp/validate_test.go similarity index 99% rename from config/openshift/v4_22_exp/validate_test.go rename to config/openshift/v4_23_exp/validate_test.go index ebdbefe8..23cac68b 100644 --- a/config/openshift/v4_22_exp/validate_test.go +++ b/config/openshift/v4_23_exp/validate_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License.) -package v4_22_exp +package v4_23_exp import ( "fmt" diff --git a/docs/config-openshift-v4_22.md b/docs/config-openshift-v4_22.md new file mode 100644 index 00000000..e0a1b23b --- /dev/null +++ b/docs/config-openshift-v4_22.md @@ -0,0 +1,191 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.22.0 +parent: Configuration specifications +nav_order: 135 +--- + +# OpenShift Specification v4.22.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.22.0` and generates Ignition configs with version `3.6.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 `