diff --git a/Makefile b/Makefile index 2f3ed33c4..955222fca 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ install: all install -m 0644 -D -t $(DESTDIR)/usr/lib/systemd/system systemd/ignition-delete-config.service install -m 0755 -D -t $(DESTDIR)/usr/lib/dracut/modules.d/30ignition bin/$(GOARCH)/ignition install -m 0755 -D -t $(DESTDIR)/usr/bin bin/$(GOARCH)/ignition-validate + install -m 0755 -D -t $(DESTDIR)/usr/bin bin/$(GOARCH)/butane install -m 0755 -d $(DESTDIR)/usr/libexec ln -sf ../lib/dracut/modules.d/30ignition/ignition $(DESTDIR)/usr/libexec/ignition-apply ln -sf ../lib/dracut/modules.d/30ignition/ignition $(DESTDIR)/usr/libexec/ignition-rmcfg diff --git a/build b/build index 9bf93d61e..7aff827b1 100755 --- a/build +++ b/build @@ -35,3 +35,8 @@ NAME="ignition-validate" echo "Building ${NAME}..." go build -ldflags "${GLDFLAGS}" -o ${BIN_PATH}/${NAME} ${REPO_PATH}/validate + +source ./butane/build + +# Restore CGO_ENABLED after butane build sets it to 0 +export CGO_ENABLED=1 diff --git a/butane/.opencode/.opencode/skills/add-sugar/SKILL.md b/butane/.opencode/.opencode/skills/add-sugar/SKILL.md new file mode 100644 index 000000000..a10618aca --- /dev/null +++ b/butane/.opencode/.opencode/skills/add-sugar/SKILL.md @@ -0,0 +1,674 @@ +--- +name: add-sugar +description: Add syntactic sugar features to Butane experimental spec versions +--- + +# Add Sugar Feature + +## What it does + +Guides and scaffolds the addition of a new syntactic sugar feature to a Butane experimental spec version: + +1. Gathers requirements from the user (spec type, field design, translation behavior) +2. Validates prerequisites (experimental spec exists, git status clean) +3. Adds struct definitions to `schema.go` +4. Implements translation (desugaring) logic in `translate.go` +5. Adds test cases in `translate_test.go` +6. Adds validation logic in `validate.go` and tests in `validate_test.go` (if needed) +7. Adds error constants to `config/common/errors.go` +8. Updates documentation descriptors in `internal/doc/butane.yaml` +9. Runs `./generate` to regenerate spec docs +10. Adds examples to `docs/examples.md` +11. Adds a release note to `docs/release-notes.md` +12. Runs `./test` to validate everything compiles and passes + +## Prerequisites + +- Go toolchain installed +- Target experimental spec version exists (directory ends with `_exp`) +- Understanding of what the sugar should do (what YAML the user writes, what Ignition config it generates) + +## Usage + +```bash +# Interactive mode - will ask for details +/add-sugar + +# Target a specific spec +/add-sugar --spec fcos/v1_8_exp --field boot_device.luks.method + +# Base spec sugar (distro-independent) +/add-sugar --spec base/v0_8_exp --field storage.files.parent +``` + +## Workflow + +### Step 1: Gather Requirements + +If not provided via arguments, ask the user: + +1. **Target spec**: Where should the sugar live? + - **Base spec** (`base/v0_8_exp`): distro-independent, will appear in all variants + - **Distro spec** (e.g., `config/fcos/v1_8_exp`, `config/openshift/v4_22_exp`): distro-specific + +2. **Feature description**: What does the sugar do? + - What YAML fields does the user write? + - What Ignition config does it expand to? + - Any validation constraints? + +3. **Schema design**: Ask the user to describe or confirm: + - Field name(s) and types + - Whether it's a new top-level field or nested within an existing struct + - Any new struct types needed + +4. **Translation approach**: Per `docs/development.md:62`: + - **Config merging** (recommended): Generate a fresh Ignition config struct, then use `baseutil.MergeTranslatedConfigs()` to merge with the user's config. The desugared struct is the merge parent, user config is child. + - **Direct modification**: Only if config merging is not expressive enough. + +5. **Validation needs**: What input constraints exist? + - Required fields + - Valid value ranges + - Mutually exclusive options + +### Step 2: Pre-flight Validation + +Run these checks: + +```bash +# Verify experimental spec directory exists +ls -la base/{version}/ || ls -la config/{distro}/{version}/ + +# Check that the version ends with _exp +# CRITICAL: Sugar must ONLY be added to experimental specs + +# Check git status +git status --porcelain +``` + +**Stop if**: +- Target spec does not exist +- Target spec is NOT experimental (name must end with `_exp`) +- Working directory has unexpected uncommitted changes + +### Step 3: Update Schema + +**File**: `{spec_dir}/schema.go` + +Read the existing schema file first to understand the current struct layout. + +#### 3a: Adding a new top-level field to Config + +If the sugar is a new top-level section (like `boot_device` or `grub`), add a field to the `Config` struct: + +```go +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` + NewSugar NewSugar `yaml:"new_sugar"` // ADD THIS +} +``` + +Then add the new struct type(s): + +```go +type NewSugar struct { + FieldOne *string `yaml:"field_one"` + FieldTwo *bool `yaml:"field_two"` +} +``` + +#### 3b: Adding a field to an existing struct in base + +If extending an existing base struct (like adding `parent` to `File`), modify the struct in `base/{version}/schema.go`: + +```go +type File struct { + // ... existing fields ... + NewField NewFieldType `yaml:"new_field"` // ADD THIS +} +``` + +**Conventions**: +- Use `*string`, `*bool`, `*int` for optional scalar fields +- Use `[]Type` for lists +- Use struct types for nested objects +- YAML tags use `snake_case` +- Add ` butane:"auto_skip"` tag for fields not in the Ignition spec that should be automatically filtered from the output (see `config/util/filter.go`) + +### Step 4: Implement Translation + +**File**: `{spec_dir}/translate.go` + +Read the existing translate.go to understand the current translation pipeline. + +#### 4a: Config Merging Pattern (Recommended) + +This is the recommended approach per `docs/development.md:62`. The desugared config is the merge parent, user config is the child, so users can override sugar-generated values. + +For **distro specs** (e.g., `config/fcos/v1_8_exp/translate.go`): + +Add a new processing function and call it from `ToIgn3_7Unvalidated()`: + +```go +func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_7Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + // Existing sugar processing... + r.Merge(c.processBootDevice(&ret, &ts, options)) + + // ADD: Call new sugar processing + retp, tsp, rp := c.processNewSugar(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + + return ret, ts, r +} +``` + +Implement the processing function: + +```go +func (c Config) processNewSugar(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // Early return if sugar is not being used + if /* sugar not configured */ { + return rendered, ts, r + } + + yamlPath := path.New("yaml", "new_sugar") + + // Generate Ignition config elements + // Example: creating a file + file := types.File{ + Node: types.Node{ + Path: "/path/to/generated/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,generated-content"), + }, + }, + } + rendered.Storage.Files = append(rendered.Storage.Files, file) + + // Track translations for error reporting + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + + return rendered, ts, r +} +``` + +For **base specs** (e.g., `base/v0_8_exp/translate.go`): + +The pattern is the same, but the processing function is called from the base `ToIgn3_7Unvalidated()` and operates on base types. When modifying translation at the base level, you may need to: +- Create or modify a custom translator function (e.g., `translateStorage()`) +- Register it with `tr.AddCustomTranslator()` + +#### 4b: Direct Modification Pattern (Alternative) + +Only use this when config merging isn't expressive enough: + +```go +func (c Config) processNewSugar(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var r report.Report + + if /* sugar not configured */ { + return r + } + + // Directly modify the Ignition config + config.Storage.Files = append(config.Storage.Files, types.File{...}) + + // Track translations + yamlPath := path.New("yaml", "new_sugar") + jsonPath := path.New("json", "storage", "files", len(config.Storage.Files)-1) + ts.AddFromCommonSource(yamlPath, jsonPath, config.Storage.Files[len(config.Storage.Files)-1]) + + return r +} +``` + +**Key imports** (add as needed): + +```go +import ( + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) +``` + +**IMPORTANT**: The Ignition types import version must match the one already used in the file. Check the existing imports before adding new ones. + +### Step 5: Write Tests + +**File**: `{spec_dir}/translate_test.go` + +Read the existing test file to understand the test patterns used. + +Tests follow a table-driven pattern. Add a new test function: + +```go +func TestTranslateNewSugar(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // empty / no-op case + { + in: Config{}, + out: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + }, + // basic sugar usage + { + in: Config{ + NewSugar: NewSugar{ + FieldOne: util.StrToPtr("value"), + }, + }, + out: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/path/to/generated/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,generated-content"), + }, + }, + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} +``` + +**IMPORTANT**: The Ignition version in test expectations (e.g., `"3.7.0-experimental"`) must match the version used in the spec's translate.go. Check the existing tests for the correct value. + +**Test categories to cover**: +- Empty/no-op: sugar not configured, should produce default output +- Basic usage: simplest valid configuration +- Complex usage: all options exercised +- User overrides: verify user can override sugar-generated values (for merge pattern) +- Edge cases: boundary conditions +- Error cases: invalid inputs (test separately in validate tests) + +### Step 6: Add Validation (If Needed) + +**File**: `{spec_dir}/validate.go` + +Read the existing validate.go to understand validation patterns. + +Add a `Validate` method on the new sugar type: + +```go +func (s NewSugar) Validate(c path.ContextPath) (r report.Report) { + if s.FieldOne != nil && *s.FieldOne == "" { + r.AddOnError(c.Append("field_one"), common.ErrNewSugarFieldOneEmpty) + } + // ... more validations + return +} +``` + +Or add validation to an existing `Validate` method on `Config`: + +```go +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + // ... existing validations ... + + // New sugar validation + if someCondition { + r.AddOnError(c.Append("new_sugar", "field"), common.ErrSomething) + } + return +} +``` + +**Validation test file**: `{spec_dir}/validate_test.go` + +```go +func TestValidateNewSugar(t *testing.T) { + tests := []struct { + in NewSugar + out error + errPath path.ContextPath + }{ + // valid config + { + in: NewSugar{FieldOne: util.StrToPtr("valid")}, + out: nil, + errPath: path.New("yaml"), + }, + // invalid config + { + in: NewSugar{FieldOne: util.StrToPtr("")}, + out: common.ErrNewSugarFieldOneEmpty, + errPath: path.New("yaml", "field_one"), + }, + } + + 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 validation report") + }) + } +} +``` + +### Step 7: Add Error Constants + +**File**: `config/common/errors.go` + +Read the existing errors.go to understand the naming pattern. + +Add new error variables in the appropriate section: + +```go +var ( + // ... existing errors ... + + // New sugar + ErrNewSugarFieldOneEmpty = errors.New("field_one must not be empty") + ErrNewSugarInvalidCombo = errors.New("field_one and field_two are mutually exclusive") +) +``` + +**Naming convention**: `Err` + CamelCase description. Error messages should be lowercase, concise, and actionable. + +### Step 8: Update Documentation Descriptors + +**File**: `internal/doc/butane.yaml` + +Read the existing butane.yaml to understand the YAML structure for field documentation. + +Add documentation descriptors for new fields. Place them in the correct location within the document hierarchy. + +For a new top-level field (sibling of `boot_device`, `grub`): + +```yaml + - name: new_sugar + after: $ + desc: describes the desired new sugar configuration. + children: + - name: field_one + desc: the value for field one. + - name: field_two + desc: whether to enable feature two. If omitted, defaults to false. +``` + +For a field within an existing section (e.g., under `storage.files`): + +```yaml + - name: files + children: + # ... existing children ... + - name: new_field + after: $ + desc: description of the new field. +``` + +**Key patterns in butane.yaml**: +- `after: $` means "add at the end" (after all Ignition-defined fields) +- `after: ^` means "add at the beginning" (before all Ignition-defined fields) +- `transforms` can conditionally modify descriptions per variant/version +- `use: component_name` reuses a named component definition +- `required: true` marks a field as required +- Limit new fields to experimental spec versions using transforms if needed (add `"Unsupported"` replacement for older versions) + +### Step 9: Regenerate Documentation + +Run the documentation generator: + +```bash +./generate +``` + +**Expected outcome**: Several `docs/config-*-exp.md` files are updated with the new field documentation. + +Verify the docs were regenerated: + +```bash +git diff docs/ +``` + +If `./generate` fails, the schema or butane.yaml likely has an error. Fix and retry. + +### Step 10: Add Usage Examples + +**File**: `docs/examples.md` + +Read the existing examples.md to understand the format. + +Add a new example section: + +```markdown +## New Sugar Feature Name + +This example {describes what the example demonstrates}. + + +```yaml +variant: fcos +version: 1.8.0-experimental +new_sugar: + field_one: value + field_two: true +``` + + +This {describes what gets generated/created}. +``` + +**Notes**: +- The `` comment markers are used for automated validation +- Use the experimental version string (e.g., `1.8.0-experimental`) +- Keep examples minimal but complete +- Show the simplest useful configuration first + +### Step 11: Add Release Notes + +**File**: `docs/release-notes.md` + +Read the current release notes section. + +Add a note under `## Upcoming Butane X.Y.Z (unreleased)` > `### Features`: + +```markdown +### Features + +- Add {sugar description} _(fcos 1.8.0-exp, openshift 4.22.0-exp, ...)_ +``` + +**Notes**: +- List all affected variants/versions in the parenthetical +- For base sugar, list all experimental variants since they all inherit it +- Use the `-exp` suffix convention for experimental versions +- Keep the description concise (one line) + +### Step 12: Run Tests + +Execute the full test suite: + +```bash +./test +``` + +**Expected outcome**: All tests pass. + +If tests fail: +1. Read the error output carefully +2. Common issues: + - Missing `TranslationSet` coverage: Add translation path tracking + - Type mismatches: Check Ignition types vs Butane types + - Import errors: Verify import paths match the experimental spec version + - Validation failures: Check that test expectations match the validation logic +3. Fix issues and re-run + +### Step 13: Report Results + +Provide a comprehensive summary: + +``` +Sugar feature "{name}" added to {spec_type}/{version} + +Files Modified: + - {spec_dir}/schema.go (+N lines) + - {spec_dir}/translate.go (+N lines) + - {spec_dir}/translate_test.go (+N lines) + - {spec_dir}/validate.go (+N lines) [if applicable] + - {spec_dir}/validate_test.go (+N lines) [if applicable] + - config/common/errors.go (+N lines) [if applicable] + - internal/doc/butane.yaml (+N lines) + - docs/examples.md (+N lines) + - docs/release-notes.md (+N lines) + - docs/config-*-exp.md (N files, regenerated) + +Tests: PASSED +Docs: REGENERATED + +Suggested commit message: + + {spec}/{version}: add {sugar_name} sugar + + {description of what the sugar does and why} + + resolves: #{issue_number} +``` + +## Checklist Coverage + +This skill guides the following workflow: + +- ✅ Schema definition in `schema.go` +- ✅ Translation logic in `translate.go` (config merging or direct modification) +- ✅ Comprehensive tests in `translate_test.go` +- ✅ Validation logic in `validate.go` (when needed) +- ✅ Validation tests in `validate_test.go` (when needed) +- ✅ Error constants in `config/common/errors.go` +- ✅ Documentation descriptors in `internal/doc/butane.yaml` +- ✅ Doc regeneration via `./generate` +- ✅ Usage examples in `docs/examples.md` +- ✅ Release notes in `docs/release-notes.md` +- ✅ Full test suite validation via `./test` + +## What's NOT Covered + +- ❌ **Designing the sugar semantics** - the user must know what Ignition config the sugar should produce +- ❌ **Complex translation logic** - the skill provides patterns, but domain-specific logic (e.g., partition layout calculations for boot_device) must be written by the developer +- ❌ **Integration tests** - only unit tests are scaffolded +- ❌ **Updating upgrading docs** - `docs/upgrading-*.md` must be updated manually when the sugar is stabilized +- ❌ **Creating git commits** - user should review changes first + +## Example Output + +``` +/add-sugar --spec fcos/v1_8_exp + +Analyzing config/fcos/v1_8_exp... + +Current experimental spec: + - Ignition version: 3.7.0-experimental + - Base dependency: base/v0_8_exp + - Existing sugar: boot_device, grub + +What sugar would you like to add? +> Network configuration shortcut for static IPs + +Gathering schema design... + +Schema: New top-level field `network` with nested structs +Translation: Config merging pattern +Validation: Required fields, IP format validation + +Phase 1: Schema + schema.go updated (+15 lines) + +Phase 2: Translation + translate.go updated (+45 lines) + +Phase 3: Tests + translate_test.go updated (+120 lines) + +Phase 4: Validation + validate.go updated (+20 lines) + validate_test.go updated (+40 lines) + +Phase 5: Errors + config/common/errors.go updated (+3 lines) + +Phase 6: Documentation + internal/doc/butane.yaml updated (+10 lines) + ./generate completed + docs/config-fcos-v1_8-exp.md regenerated + docs/config-fiot-v1_1-exp.md regenerated + docs/config-flatcar-v1_2-exp.md regenerated + docs/config-openshift-v4_22-exp.md regenerated + docs/config-r4e-v1_2-exp.md regenerated + +Phase 7: Examples & Release Notes + docs/examples.md updated (+12 lines) + docs/release-notes.md updated (+1 line) + +Phase 8: Validation + ./test: All tests passed + +Sugar feature "network" added to fcos/v1_8_exp + +Suggested commit message: + + fcos/v1_8_exp: add network configuration sugar + + Add a `network` section that allows users to configure static + IP addresses without manually creating NetworkManager keyfiles. + + resolves: #XXX +``` + +## References + +- Design document: `.opencode/skills/add-sugar/DESIGN.md` +- Examples: `.opencode/skills/add-sugar/examples/` +- Development guide: `docs/development.md` (esp. lines 60-64 on sugar implementation) +- Stabilize checklist: `.github/ISSUE_TEMPLATE/stabilize-checklist.md` +- Current base experimental: `base/v0_8_exp/` +- Current FCOS experimental: `config/fcos/v1_8_exp/` +- Current OpenShift experimental: `config/openshift/v4_22_exp/` diff --git a/butane/.opencode/.opencode/skills/remove-feature/SKILL.md b/butane/.opencode/.opencode/skills/remove-feature/SKILL.md new file mode 100644 index 000000000..5f7369781 --- /dev/null +++ b/butane/.opencode/.opencode/skills/remove-feature/SKILL.md @@ -0,0 +1,326 @@ +--- +name: remove-feature +description: Remove unsupported sugar features from stabilized OpenShift spec versions +--- + +# Remove Feature from Stabilized Spec + +## What it does + +Automates the removal of sugar features from stabilized OpenShift spec versions: + +1. Validates the target spec directory and feature existence +2. Removes the feature's translation function call from `translate.go` +3. Removes the translation function definition from `translate.go` +4. Removes related test cases from `translate_test.go` +5. Bumps the `max` version in `internal/doc/butane.yaml` for doc transforms +6. Runs `./generate` to regenerate spec documentation +7. Runs `./test` to validate all changes + +## Prerequisites + +- Go toolchain installed +- Target spec version is stabilized (directory does NOT end with `_exp`) +- Feature exists in the target spec's `translate.go` +- Feature has doc transform entries in `internal/doc/butane.yaml` + +## Usage + +```bash +# Interactive mode - will ask for details +/remove-feature + +# Specify the version and feature +/remove-feature --distro openshift --version v4_22 --feature grub + +# With tracking reference +/remove-feature --distro openshift --version v4_22 --feature grub --ref MCO-630 +``` + +## Workflow + +### Step 1: Gather Requirements + +If not provided via arguments, ask the user: + +1. **Distro**: Which distro variant? (typically `openshift`) +2. **Version**: Which stabilized version? (e.g., `v4_22`) +3. **Feature**: Which feature to remove? (e.g., `grub`) +4. **Reference**: Optional tracking issue (e.g., `MCO-630`, `#515`) + +### Step 2: Pre-flight Validation + +Run these checks: + +```bash +# Verify target directory exists and is NOT experimental +ls -la config/{distro}/{version}/ + +# Verify version does NOT end with _exp +# CRITICAL: Only remove features from stabilized specs + +# Check git status +git status --porcelain +``` + +**Stop if**: +- Target directory does not exist +- Version ends with `_exp` (experimental specs should be modified differently) +- Working directory has unexpected uncommitted changes + +### Step 3: Identify Feature Code + +Read the target files to locate the feature: + +```bash +# Read translate.go to find feature function +cat config/{distro}/{version}/translate.go + +# Read translate_test.go to find test cases +cat config/{distro}/{version}/translate_test.go + +# Read butane.yaml to find doc transform entries +cat internal/doc/butane.yaml +``` + +**Identify**: +1. The function call in the main translation pipeline (e.g., `ts = translateUserGrubCfg(&cfg, &ts)`) +2. The function definition (e.g., `func translateUserGrubCfg(...)`) +3. The test case block (e.g., `// Test Grub config` test struct) +4. The butane.yaml transform entries with `replacement: "Unsupported"` and `max` version + +**Stop if** the feature code is not found in `translate.go` - it may have already been removed or never existed in this version. + +### Step 4: Remove Translation Function Call + +**File**: `config/{distro}/{version}/translate.go` + +Read the file and find the function call within the main `ToMachineConfig{Version}Unvalidated()` function. + +Remove the line calling the feature's translation function. For example: + +```go +// REMOVE THIS LINE: +ts = translateUserGrubCfg(&cfg, &ts) +``` + +Use the Edit tool: +``` +oldString: "\tts = translateUserGrubCfg(&cfg, &ts)\n" +newString: "" +``` + +### Step 5: Remove Translation Function Definition + +**File**: `config/{distro}/{version}/translate.go` + +Remove the entire function definition. The function is typically at the end of the file. + +For the GRUB removal pattern, remove the entire `translateUserGrubCfg` function: + +```go +// REMOVE THIS ENTIRE BLOCK: + +// fcos config generates a user.cfg file using append; however, OpenShift config +// does not support append (since MCO does not support it). Let change the file to use contents +func translateUserGrubCfg(config *types.Config, ts *translate.TranslationSet) translate.TranslationSet { + // ... function body ... +} +``` + +Use the Edit tool to remove from the comment above the function through the closing brace. + +### Step 6: Clean Up Dead Imports + +After removing the function, check if any imports are now unused. Common imports that may become dead: + +- For GRUB removal: no imports typically become dead (the remaining code uses the same packages) + +If imports are dead, remove them. Run `./test` later to catch any remaining issues. + +### Step 7: Remove Test Cases + +**File**: `config/{distro}/{version}/translate_test.go` + +Read the file and identify the test case block for the removed feature. Test cases are typically marked with a comment like `// Test Grub config` and consist of a struct literal in the test table. + +Remove the entire test case struct. For GRUB removal, this includes: +- The comment marker (e.g., `// Test Grub config`) +- The Config input struct +- The expected result.MachineConfig output struct +- The expected translate.Translation slice + +Use the Edit tool to remove the entire block. Be careful to: +- Include the leading comment +- Include all three struct elements (input, expected output, expected translations) +- Preserve the closing of the test table (`}` and `for` loop) + +### Step 8: Update Doc Descriptors + +**File**: `internal/doc/butane.yaml` + +Find the doc transform entries for the removed feature. These have the pattern: + +```yaml +transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: openshift + max: {PREVIOUS_VERSION} +``` + +**Determine the new max version**: +- From the directory version `v4_22`, derive `4.22.0` +- The current `max` should be the previous stabilized version (e.g., `4.21.0`) +- Bump `max` to the new version: `4.22.0` + +**Update ALL occurrences** for the feature. For GRUB, there are 4 entries: +1. `grub` itself +2. `grub.users` +3. `grub.users.name` +4. `grub.users.password_hash` + +Use the Edit tool for each occurrence: +``` +oldString: "max: 4.21.0" +newString: "max: 4.22.0" +``` + +**IMPORTANT**: Only update `max` values within the feature's section. Verify the surrounding context (field names) to avoid changing unrelated transforms. + +Alternatively, if all occurrences have the same old value, use `replaceAll` with enough context to scope the changes correctly. + +### Step 9: Regenerate Documentation + +Run the documentation generator: + +```bash +./generate +``` + +**Expected outcome**: The `docs/config-openshift-{version}.md` file is updated, with the removed feature's fields now showing "Unsupported" instead of their original descriptions. + +Verify the change: + +```bash +git diff docs/config-openshift-{version}.md +``` + +The diff should show lines like: +``` +-* **_grub_** (object): describes the desired GRUB bootloader configuration. ++* **_grub_** (object): Unsupported +``` + +If `./generate` fails, check the butane.yaml changes for syntax errors. + +### Step 10: Run Tests + +```bash +./test +``` + +**Expected outcome**: All tests pass. + +If tests fail: +1. Check for compilation errors (dead imports, missing functions) +2. Check for test expectation mismatches +3. Fix and re-run + +### Step 11: Report Results + +Provide a comprehensive summary: + +``` +Feature "{feature}" removed from {distro}/{version} + +Files Modified: + - config/{distro}/{version}/translate.go (-N lines) + - config/{distro}/{version}/translate_test.go (-N lines) + - internal/doc/butane.yaml (N version bumps) + - docs/config-{distro}-{version}.md (regenerated, N fields -> "Unsupported") + +Tests: PASSED +Docs: REGENERATED + +Suggested commit message: + + {distro}/{version}: Remove {feature_description} + + {reason for removal, e.g., "Support is still missing in the MCO."} + + See: {reference_link} + See: #{github_issue} +``` + +## Checklist Coverage + +This skill automates the following steps: + +- ✅ Remove feature translation function call from `translate.go` +- ✅ Remove feature translation function definition from `translate.go` +- ✅ Remove related test cases from `translate_test.go` +- ✅ Bump `max` version in `internal/doc/butane.yaml` for all feature fields +- ✅ Regenerate documentation via `./generate` +- ✅ Validate changes via `./test` + +## What's NOT Covered + +- ❌ **Determining which features to remove** - requires knowledge of MCO support status +- ❌ **Removing features from experimental specs** - experimental specs should use different approaches +- ❌ **Removing schema definitions** - this skill only removes translation/test code; schemas are inherited from parent and remain (they just become dead code for that version) +- ❌ **Removing validation logic** - if the feature has validation in `validate.go`, that must be handled separately +- ❌ **Creating git commits** - user should review changes first +- ❌ **Updating release notes** - user should note the removal if appropriate + +## Example Output + +``` +/remove-feature --distro openshift --version v4_22 --feature grub --ref MCO-630 + +Validating prerequisites... +✅ Target directory exists: config/openshift/v4_22 +✅ Version is stabilized (not experimental) +✅ Git working directory is clean + +Identifying feature code... +✅ Found function call: ts = translateUserGrubCfg(&cfg, &ts) +✅ Found function definition: translateUserGrubCfg (21 lines) +✅ Found test case: // Test Grub config (83 lines) +✅ Found 4 butane.yaml transform entries (max: 4.21.0) + +Phase 1: Remove translation code + ✅ Removed function call from translate.go + ✅ Removed function definition from translate.go (-22 lines) + +Phase 2: Remove test cases + ✅ Removed test case from translate_test.go (-83 lines) + +Phase 3: Update doc descriptors + ✅ Bumped max: 4.21.0 → 4.22.0 for grub (4 entries) + +Phase 4: Regenerate docs + ✅ ./generate completed + ✅ docs/config-openshift-v4_22.md updated (4 fields → "Unsupported") + +Phase 5: Validate + ✅ ./test passed + +Feature "grub" removed from openshift/v4_22 + +Suggested commit message: + + openshift/v4_22: Remove GRUB config support + + See: https://issues.redhat.com/browse/MCO-630 + See: #515 +``` + +## References + +- Design document: `.opencode/skills/remove-feature/DESIGN.md` +- Examples: `.opencode/skills/remove-feature/examples/` +- Example commits: `4a2be91`, `2d9a25e`, `aa6ad0b`, `9821f9b`, `cd75f80`, `1f65fb6` +- Current experimental spec with GRUB code: `config/openshift/v4_22_exp/translate.go:264-285` +- Doc descriptors: `internal/doc/butane.yaml:402-438` diff --git a/butane/.opencode/.opencode/skills/stabilize-spec/SKILL.md b/butane/.opencode/.opencode/skills/stabilize-spec/SKILL.md new file mode 100644 index 000000000..79d20b5c1 --- /dev/null +++ b/butane/.opencode/.opencode/skills/stabilize-spec/SKILL.md @@ -0,0 +1,597 @@ +--- +name: stabilize-spec +description: Stabilize experimental Butane config spec versions +--- + +# Stabilize Spec Version + +## What it does + +Automates the complete stabilization workflow for Butane spec versions: + +**Phase 1: Stabilize Experimental → Stable** +1. Validating the working directory and checking prerequisites +2. Renaming the experimental directory to stable (e.g., `v1_7_exp` → `v1_7`) +3. Updating all package statements in the renamed directory +4. Updating imports in the stabilized spec (base dependencies, Ignition versions) +5. Updating `config/config.go` registration (for distro specs only) + +**Phase 2: Create Next Experimental Version** +6. Copying the newly stabilized version to create the next experimental version (e.g., `v1_7` → `v1_8_exp`) +7. Updating package statements in the new experimental directory +8. Bumping base and Ignition dependencies to experimental versions +9. Registering the new experimental version in `config/config.go` + +**Phase 3: Validation & Documentation** +10. Running tests to validate all changes +11. Running `./generate` to update documentation +12. Reporting all changes and suggesting next steps + +## Prerequisites + +- Clean git working directory (or only expected changes) +- Go toolchain installed +- Experimental spec version exists +- Target stable version doesn't already exist + +## Usage + +```bash +# Stabilize a base version (creates v0_7 stable + v0_8_exp experimental) +/stabilize-spec --type base --version v0_7_exp + +# Stabilize a distro version (creates v1_7 stable + v1_8_exp experimental) +/stabilize-spec --type fcos --version v1_7_exp + +# Stabilize a distro version with Ignition downgrade +/stabilize-spec --type openshift --version v4_21_exp --base-version v1_6 --ignition-version v3_5 + +# Skip creating the next experimental version (not recommended) +/stabilize-spec --type fcos --version v1_7_exp --skip-next-exp +``` + +## Workflow + +### Step 1: Gather Requirements + +If not provided via arguments, ask the user: + +1. **Spec type**: base, fcos, openshift, flatcar, r4e, or fiot? +2. **Version to stabilize**: Which experimental version? (e.g., `v1_7_exp`, `v4_21_exp`) +3. **For distro specs only**: + - Which stable base version to depend on? (e.g., `v1_6`, `v0_6`) + - Does the Ignition version change? If yes, what's the target? (e.g., `v3_5`) +4. **Create next experimental version?** (default: yes, per stabilize-checklist.md) + - If yes, calculate the next version number (e.g., v1_7 → v1_8_exp) + - Ask which experimental base to use (e.g., v0_8_exp) + - Ask which experimental Ignition version to use (e.g., v3_7_experimental) + +### Step 2: Pre-flight Validation + +Run these checks in parallel: + +```bash +# Check git status +git status --porcelain + +# Verify experimental directory exists +ls -la base/{version}/ || ls -la config/{distro}/{version}/ + +# Verify stable directory doesn't exist +ls -la base/{stable_version}/ || ls -la config/{distro}/{stable_version}/ + +# For distro specs: verify base version exists +ls -la base/{base_version}/ || ls -la config/fcos/{base_version}/ +``` + +**Validation criteria**: +- Git working directory should be clean or only contain expected changes +- Source experimental directory must exist +- Target stable directory must NOT exist +- If distro spec: base dependency must exist + +If any check fails, report the error and stop. + +### Step 3: Rename Directory + +Use `git mv` to rename the experimental directory: + +```bash +# For base specs: +git mv base/{version} base/{stable_version} + +# For distro specs: +git mv config/{distro}/{version} config/{distro}/{stable_version} +``` + +**Example**: +```bash +git mv config/fcos/v1_7_exp config/fcos/v1_7 +``` + +### Step 4: Update Package Statements + +Find all `.go` files in the renamed directory and update package statements: + +```bash +# Find all .go files +find {renamed_directory} -name "*.go" + +# For each file, update the package statement +# OLD: package v1_7_exp +# NEW: package v1_7 +``` + +Use the Edit tool to replace: +``` +oldString: "package {version}" +newString: "package {stable_version}" +``` + +**Files typically affected**: +- Base specs: schema.go, translate.go, translate_test.go, util.go, validate.go, validate_test.go (6 files) +- Distro specs: schema.go, translate.go, translate_test.go, validate.go, validate_test.go (5-7 files) + +### Step 5: Update Imports (Distro Specs Only) + +For distro specs, update base dependency imports: + +1. **Identify files that import base**: + - schema.go + - translate_test.go + - validate.go + - validate_test.go + +2. **Update base import**: +```go +// OLD: +import ( + base "github.com/coreos/butane/base/v0_7_exp" +) + +// NEW: +import ( + base "github.com/coreos/butane/base/v0_6" +) +``` + +3. **For OpenShift specs, also update fcos import in schema.go**: +```go +// OLD: +import ( + fcos "github.com/coreos/butane/config/fcos/v1_7_exp" +) + +// NEW: +import ( + fcos "github.com/coreos/butane/config/fcos/v1_6" +) +``` + +### Step 6: Update Ignition Imports (If Version Changes) + +If the Ignition version is changing (common for OpenShift stabilizations): + +1. **Find files that import Ignition types**: + - result/schema.go (OpenShift only) + - translate.go + - translate_test.go + +2. **Update Ignition imports**: +```go +// OLD: +import ( + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" +) + +// NEW: +import ( + "github.com/coreos/ignition/v2/config/v3_5/types" +) +``` + +3. **Rename translation functions in translate.go**: + - `ToIgn3_6Unvalidated` → `ToIgn3_5Unvalidated` + - `ToIgn3_6` → `ToIgn3_5` + - Update function comments + - Update `cutil.Translate` and `cutil.TranslateBytes` calls + +4. **Update test version strings in translate_test.go**: +```go +// OLD: +Version: "3.6.0-experimental", + +// NEW: +Version: "3.5.0", +``` + +### Step 7: Update config/config.go (Distro Specs Only) + +For distro specs, update the registration in `config/config.go`: + +1. **Update import statement**: +```go +// OLD: +import ( + fcos1_7_exp "github.com/coreos/butane/config/fcos/v1_7_exp" +) + +// NEW: +import ( + fcos1_7 "github.com/coreos/butane/config/fcos/v1_7" +) +``` + +2. **Update RegisterTranslator call in init()**: +```go +// OLD: +RegisterTranslator("fcos", "1.7.0-experimental", fcos1_7_exp.ToIgn3_6Bytes) + +// NEW: +RegisterTranslator("fcos", "1.7.0", fcos1_7.ToIgn3_6Bytes) +``` + +**Pattern**: Remove `-experimental` suffix from version string, update import alias + +### Step 8: Create Next Experimental Version + +**Note**: This step implements lines 20-21 (base) and 28-30 (distro) from `.github/ISSUE_TEMPLATE/stabilize-checklist.md`. + +If `--skip-next-exp` was NOT specified (default behavior): + +#### 8a. Calculate Next Version + +Determine the next experimental version: +- For base: `v0_7` → `v0_8_exp` +- For fcos: `v1_7` → `v1_8_exp` +- For openshift: `v4_21` → `v4_22_exp` + +Parse the stable version number and increment it. + +#### 8b. Copy Stable to New Experimental + +Use `cp -r` or recursive copy to duplicate the newly stabilized directory: + +```bash +# For base specs: +cp -r base/{stable_version} base/{next_exp_version} + +# For distro specs: +cp -r config/{distro}/{stable_version} config/{distro}/{next_exp_version} + +# Then add to git: +git add base/{next_exp_version} || git add config/{distro}/{next_exp_version} +``` + +**Example**: +```bash +cp -r config/fcos/v1_7 config/fcos/v1_8_exp +git add config/fcos/v1_8_exp +``` + +#### 8c. Update Package Statements in New Experimental + +Find all `.go` files in the new experimental directory and update package statements: + +```bash +# For each .go file in the new experimental directory: +# OLD: package v1_7 +# NEW: package v1_8_exp +``` + +Use the Edit tool to replace in each file. + +#### 8d. Update Base Dependency (Distro Specs Only) + +For distro specs, update the base import to use the new experimental base: + +```go +// In schema.go, translate_test.go, validate.go, validate_test.go: +// OLD: +import ( + base "github.com/coreos/butane/base/v0_7" +) + +// NEW: +import ( + base "github.com/coreos/butane/base/v0_8_exp" +) +``` + +**For OpenShift**, also update fcos import in schema.go if fcos has a new experimental version. + +#### 8e. Update Ignition Imports (If Version Increases) + +If the Ignition version is increasing (e.g., v3_6 → v3_7_experimental): + +1. **Update Ignition imports in**: + - result/schema.go (OpenShift only) + - translate.go + - translate_test.go + +```go +// OLD: +import ( + "github.com/coreos/ignition/v2/config/v3_6/types" +) + +// NEW: +import ( + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" +) +``` + +2. **Rename translation functions in translate.go**: + - `ToIgn3_6Unvalidated` → `ToIgn3_7Unvalidated` + - `ToIgn3_6` → `ToIgn3_7` + - Update function comments + - Update `cutil.Translate` and `cutil.TranslateBytes` calls + +3. **Update test version strings in translate_test.go**: +```go +// OLD: +Version: "3.6.0", + +// NEW: +Version: "3.7.0-experimental", +``` + +#### 8f. Add New Experimental to config/config.go (Distro Specs Only) + +For distro specs, register the new experimental version in `config/config.go`: + +1. **Add import statement**: +```go +// After the just-stabilized import: +import ( + fcos1_7 "github.com/coreos/butane/config/fcos/v1_7" + fcos1_8_exp "github.com/coreos/butane/config/fcos/v1_8_exp" // ADD THIS +) +``` + +2. **Add RegisterTranslator call in init()**: +```go +// After the just-stabilized registration: +RegisterTranslator("fcos", "1.7.0", fcos1_7.ToIgn3_6Bytes) +RegisterTranslator("fcos", "1.8.0-experimental", fcos1_8_exp.ToIgn3_7Bytes) // ADD THIS +``` + +**Pattern**: Add `-experimental` suffix, use experimental Ignition version + +### Step 9: Run Tests + +Execute the test suite to validate all changes: + +```bash +./test +``` + +**Expected outcome**: All tests should pass. + +If tests fail: +- Review the error messages +- Check for missed imports or package statements +- Verify function renames are complete +- Report the failure to the user and suggest manual review + +### Step 10: Regenerate Documentation + +Run the documentation generator: + +```bash +./generate +``` + +**Expected outcome**: Documentation files in `docs/` are updated. + +Check for uncommitted changes: +```bash +git status docs/ +``` + +If `./generate` fails or produces unexpected changes, report to the user. + +### Step 11: Report Results + +Provide a comprehensive summary: + +``` +✅ Spec stabilization complete! + +## Phase 1: Stabilization +### Directory Renamed: +- {old_path} → {new_path} + +### Files Modified: +- {count} files with package statement updates +- {count} files with import updates +- config/config.go updated (distro specs only) + +## Phase 2: Next Experimental Version Created +### Directory Created: +- {new_exp_path} + +### Files Modified: +- {count} files with package statement updates +- {count} files with import updates (bumped to experimental versions) +- config/config.go updated with new experimental registration + +## Validation: +✅ Tests passed (./test) +✅ Documentation regenerated (./generate) + +## Git Status: +{output of git status} + +## Next Steps (from stabilize-checklist.md): + +1. Review the changes with `git diff` +2. Consider creating TWO commits: + - Commit 1: Stabilization (e.g., "fcos/v1_7_exp: stabilize to v1_7") + - Commit 2: New experimental (e.g., "fcos: add v1_8_exp") +3. Update docs/upgrading-*.md (requires manual content creation) +4. Note the stabilization in docs/release-notes.md +5. If this is a base stabilization, stabilize the distro versions that depend on it + +## Suggested commit messages: + +### Commit 1: Stabilization +{distro}/v{X}_exp: stabilize to v{X} + +- Rename {distro}/v{X}_exp to {distro}/v{X} +- Update package statements and imports +- Drop -experimental from config registration +{additional details based on what changed} + +### Commit 2: New Experimental +{distro}: add v{X+1}_exp + +- Copy {distro}/v{X} to {distro}/v{X+1}_exp +- Update package statements to v{X+1}_exp +- Bump base dependency to {base}_exp +- Bump Ignition version to v{Y}_experimental +- Add experimental config registration +``` + +## Checklist Coverage + +This skill automates the following items from `.github/ISSUE_TEMPLATE/stabilize-checklist.md`: + +### For Base Stabilization (lines 17-21): +- ✅ Rename `base/vB_exp` to `base/vB` and update `package` statements +- ✅ Update imports +- ✅ **Copy `base/vB` to `base/vB+1_exp`** +- ✅ **Update `package` statements in `base/vB+1_exp`** + +### For Distro Stabilization (lines 23-30): +- ✅ Rename `config/distro/vD_exp` to `config/distro/vD` and update `package` statements +- ✅ Update imports +- ✅ Drop `-experimental` from `init()` in `config/config.go` +- ✅ **Copy `config/distro/vD` to `config/distro/vD+1_exp`** +- ✅ **Update `package` statements in `config/distro/vD+1_exp`** +- ✅ **Bump base dependency to `base/vB+1_exp`** +- ✅ **Import `config/vD+1_exp` in `config/config.go` and add experimental registration** + +### For Ignition Spec Version Bumps (lines 32-35): +- ✅ Bump Ignition types imports in new experimental version +- ✅ Rename `ToIgnI` functions in new experimental version +- ✅ Bump Ignition spec versions in translate_test.go + +### For Documentation (lines 37-40): +- ✅ Run `./generate` to regenerate spec docs + +## What's NOT Covered + +This skill does NOT automate: + +- ❌ **Bumping go.mod for Ignition releases** - done before stabilization (line 14) +- ❌ **Updating vendor directory** - done before stabilization (line 14) +- ❌ **Dropping -experimental from examples in docs/** - requires content analysis (line 27) +- ❌ **Updating `internal/doc/main.go`** - requires manual editing (line 39) +- ❌ **Updating docs/specs.md** - requires manual editing (line 41) +- ❌ **Updating docs/upgrading-*.md** - requires content creation (line 42) +- ❌ **Writing release notes** - requires human judgment (line 43) +- ❌ **Creating git commits** - user should review changes first + +These steps require human judgment and should be done manually following the stabilization. + +## Example Output + +``` +/stabilize-spec --type fcos --version v1_7_exp + +═══════════════════════════════════════════════ + PHASE 1: Stabilize v1_7_exp → v1_7 +═══════════════════════════════════════════════ + +Validating prerequisites... +✅ Git working directory is clean +✅ Experimental version exists: config/fcos/v1_7_exp +✅ Stable version doesn't exist: config/fcos/v1_7 +✅ Base dependency exists: base/v0_7 + +Renaming directory... +✅ Renamed: config/fcos/v1_7_exp → config/fcos/v1_7 + +Updating package statements (5 files)... +✅ config/fcos/v1_7/schema.go +✅ config/fcos/v1_7/translate.go +✅ config/fcos/v1_7/translate_test.go +✅ config/fcos/v1_7/validate.go +✅ config/fcos/v1_7/validate_test.go + +Updating imports... +✅ Updated base import in 4 files + +Updating config/config.go... +✅ Import statement updated: fcos1_7_exp → fcos1_7 +✅ Registration updated: removed -experimental suffix + +═══════════════════════════════════════════════ + PHASE 2: Create Next Experimental v1_8_exp +═══════════════════════════════════════════════ + +Calculating next version... +✅ Next version: v1_8_exp +✅ Next experimental base: v0_8_exp +✅ Next Ignition version: v3_7_experimental + +Copying stable to experimental... +✅ Copied: config/fcos/v1_7 → config/fcos/v1_8_exp + +Updating package statements (5 files)... +✅ config/fcos/v1_8_exp/schema.go +✅ config/fcos/v1_8_exp/translate.go +✅ config/fcos/v1_8_exp/translate_test.go +✅ config/fcos/v1_8_exp/validate.go +✅ config/fcos/v1_8_exp/validate_test.go + +Updating imports to experimental versions... +✅ Updated base import: v0_7 → v0_8_exp (4 files) +✅ Updated Ignition import: v3_6 → v3_7_experimental (2 files) + +Updating translation functions... +✅ Renamed: ToIgn3_6Unvalidated → ToIgn3_7Unvalidated +✅ Renamed: ToIgn3_6 → ToIgn3_7 +✅ Updated: ToIgn3_6Bytes → ToIgn3_7Bytes + +Updating config/config.go... +✅ Added import: fcos1_8_exp +✅ Added registration: 1.8.0-experimental + +═══════════════════════════════════════════════ + PHASE 3: Validation +═══════════════════════════════════════════════ + +Running tests... +✅ All tests passed (./test) + +Regenerating documentation... +✅ Documentation updated (./generate) + +═══════════════════════════════════════════════ + SUMMARY +═══════════════════════════════════════════════ + +📊 Phase 1 (Stabilization): + - 1 directory renamed + - 5 files updated in config/fcos/v1_7/ + - config/config.go updated + +📊 Phase 2 (New Experimental): + - 1 directory created (2874 lines) + - 5 files updated in config/fcos/v1_8_exp/ + - config/config.go updated + +✅ Tests: PASSED +✅ Docs: REGENERATED + +🎯 Ready for review and commit! +``` + +## References + +- Design document: `.opencode/skills/stabilize-spec/DESIGN.md` +- Examples: `.opencode/skills/stabilize-spec/examples/` +- Issue template: `.github/ISSUE_TEMPLATE/stabilize-checklist.md` +- Development docs: `docs/development.md` diff --git a/butane/Dockerfile b/butane/Dockerfile new file mode 100644 index 000000000..254d5903c --- /dev/null +++ b/butane/Dockerfile @@ -0,0 +1,10 @@ +FROM quay.io/fedora/fedora:44 AS builder +RUN dnf install -y golang git-core +RUN mkdir /butane +COPY . /butane +WORKDIR /butane +RUN ./build_for_container + +FROM quay.io/fedora/fedora-minimal:44 +COPY --from=builder /butane/bin/container/butane /usr/local/bin/butane +ENTRYPOINT ["/usr/local/bin/butane"] diff --git a/butane/LICENSE b/butane/LICENSE new file mode 100644 index 000000000..e06d20818 --- /dev/null +++ b/butane/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/butane/NEWS.md b/butane/NEWS.md new file mode 120000 index 000000000..9f024e93a --- /dev/null +++ b/butane/NEWS.md @@ -0,0 +1 @@ +docs/release-notes.md \ No newline at end of file diff --git a/butane/README.md b/butane/README.md new file mode 100644 index 000000000..1291acf4a --- /dev/null +++ b/butane/README.md @@ -0,0 +1,9 @@ +# Butane + +Butane (formerly the Fedora CoreOS Config Transpiler, FCCT) translates human readable Butane Configs +into machine readable [Ignition](https://github.com/coreos/ignition) Configs. See the [getting +started](docs/getting-started.md) guide for how to use Butane and the [configuration +specifications](docs/specs.md) for everything Butane configs support. + +For information on developing Butane, using it as a library, or understanding how the binaries released +in this repository are built, see the [development docs](docs/development.md). diff --git a/butane/base/util/file.go b/butane/base/util/file.go new file mode 100644 index 000000000..f3f2c5855 --- /dev/null +++ b/butane/base/util/file.go @@ -0,0 +1,160 @@ +// 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 util + +import ( + "os" + "path/filepath" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" +) + +func EnsurePathWithinFilesDir(path, filesDir string) error { + absBase, err := filepath.Abs(filesDir) + if err != nil { + return err + } + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + if absPath != absBase && !strings.HasPrefix(absPath, absBase+string(filepath.Separator)) { + return common.ErrFilesDirEscape + } + return nil +} + +func ReadLocalFile(configPath, filesDir string) ([]byte, error) { + if filesDir == "" { + // a files dir isn't configured; refuse to read anything + return nil, common.ErrNoFilesDir + } + // calculate file path within FilesDir and check for path traversal + filePath := filepath.Join(filesDir, filepath.FromSlash(configPath)) + if err := EnsurePathWithinFilesDir(filePath, filesDir); err != nil { + return nil, err + } + return os.ReadFile(filePath) +} + +// CheckForDecimalMode fails if the specified mode appears to have been +// incorrectly specified in decimal instead of octal. +func CheckForDecimalMode(mode int, directory bool) error { + correctedMode, ok := decimalModeToOctal(mode) + if !ok { + return nil + } + if !isTypicalMode(mode, directory) && isTypicalMode(correctedMode, directory) { + return common.ErrDecimalMode + } + return nil +} + +// isTypicalMode returns true if the specified mode is unsurprising. +// It returns false for some modes that are unusual but valid in limited +// cases. +func isTypicalMode(mode int, directory bool) bool { + // no permissions is always reasonable (root ignores mode bits) + if mode == 0 { + return true + } + + // test user/group/other in reverse order + perms := []int{mode & 0007, (mode & 0070) >> 3, (mode & 0700) >> 6} + hadR := false + hadW := false + hadX := false + for _, perm := range perms { + r := perm&4 != 0 + w := perm&2 != 0 + x := perm&1 != 0 + // more-specific perm must have all the bits of less-specific + // perm (r--rw----) + if !r && hadR || !w && hadW || !x && hadX { + return false + } + // if we have executable permission, it's weird for a + // less-specific perm to have read but not execute (rwxr-----) + if x && hadR && !hadX { + return false + } + // -w- and --x are reasonable in special cases but they're + // uncommon + if (w || x) && !r { + return false + } + hadR = hadR || r + hadW = hadW || w + hadX = hadX || x + } + + // must be readable by someone + if !hadR { + return false + } + + if directory { + // must be executable by someone + if !hadX { + return false + } + // setuid forbidden + if mode&04000 != 0 { + return false + } + // setgid or sticky must be writable to someone + if mode&03000 != 0 && !hadW { + return false + } + } else { + // setuid or setgid + if mode&06000 != 0 { + // must be executable to someone + if !hadX { + return false + } + // world-writable permission is a bad idea + if mode&2 != 0 { + return false + } + } + // sticky forbidden + if mode&01000 != 0 { + return false + } + } + + return true +} + +// decimalModeToOctal takes a mode written in decimal and converts it to +// octal, returning (0, false) on failure. +func decimalModeToOctal(mode int) (int, bool) { + if mode < 0 || mode > 7777 { + // out of range + return 0, false + } + ret := 0 + for divisor := 1000; divisor > 0; divisor /= 10 { + digit := (mode / divisor) % 10 + if digit > 7 { + // digit not available in octal + return 0, false + } + ret = (ret << 3) | digit + } + return ret, true +} diff --git a/butane/base/util/file_test.go b/butane/base/util/file_test.go new file mode 100644 index 000000000..637e79cb3 --- /dev/null +++ b/butane/base/util/file_test.go @@ -0,0 +1,128 @@ +// 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 util + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + expectedBadDirModes = []int{ + 500, // 0764 + 550, // 01046 + 555, // 01053 + 700, // 01274 + 750, // 01356 + 755, // 01363 + 770, // 01402 + 775, // 01407 + 777, // 01411 + 1700, // 03244 + 1750, // 03326 + 1755, // 03333 + 1770, // 03352 + 1775, // 03357 + 1777, // 03361 + 2700, // 05214 + 2750, // 05276 + 2755, // 05303 + 2770, // 05322 + 2775, // 05327 + 2777, // 05331 + 3700, // 07164 + 3750, // 07246 + 3755, // 07253 + 3770, // 07272 + 3775, // 07277 + 3777, // 07301 + } + expectedBadFileModes = []int{ + 400, // 0620 + 440, // 0670 + 444, // 0674 + 500, // 0764 + 550, // 01046 + 555, // 01053 + 600, // 01130 + 640, // 01200 + 644, // 01204 + 660, // 01224 + 664, // 01230 + 666, // 01232 + 700, // 01274 + 750, // 01356 + 755, // 01363 + 770, // 01402 + 775, // 01407 + 777, // 01411 + 2500, // 04704 + 2550, // 04766 + 2555, // 04773 + 2700, // 05214 + 2750, // 05276 + 2755, // 05303 + 2770, // 05322 + 2775, // 05327 + 4500, // 010624 + 4550, // 010706 + 4555, // 010713 + 4700, // 011134 + 4750, // 011216 + 4755, // 011223 + 4770, // 011242 + 4775, // 011247 + 6500, // 014544 + 6550, // 014626 + 6555, // 014633 + 6700, // 015054 + 6750, // 015136 + 6755, // 015143 + 6770, // 015162 + 6775, // 015167 + } +) + +func TestCheckForDecimalMode(t *testing.T) { + // test decimal to octal conversion + for i := -1; i < 10001; i++ { + t.Run(fmt.Sprintf("mode %d", i), func(t *testing.T) { + iStr := fmt.Sprintf("%d", i) + result, ok := decimalModeToOctal(i) + + assert.Equal(t, i >= 0 && i <= 7777 && !strings.ContainsAny(iStr, "89"), ok, "converting to octal returned incorrect ok") + if ok { + assert.Equal(t, iStr, fmt.Sprintf("%o", result), "converting to octal failed") + } + }) + } + + // check the checker against a hardcoded list + var badDirModes []int + var badFileModes []int + for i := -1; i <= 10000; i++ { + if CheckForDecimalMode(i, true) != nil { + badDirModes = append(badDirModes, i) + } + if CheckForDecimalMode(i, false) != nil { + badFileModes = append(badFileModes, i) + } + } + assert.Equal(t, expectedBadDirModes, badDirModes, "bad set of decimal directory modes") + assert.Equal(t, expectedBadFileModes, badFileModes, "bad set of decimal file modes") +} diff --git a/butane/base/util/merge.go b/butane/base/util/merge.go new file mode 100644 index 000000000..d0a021f9a --- /dev/null +++ b/butane/base/util/merge.go @@ -0,0 +1,75 @@ +// 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 util + +import ( + "fmt" + + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/merge" +) + +// MergeTranslatedConfigs merges a parent and child config and returns the +// result. It also generates and returns the merged TranslationSet by +// mapping the parent/child TranslationSets through the merge transcript. +func MergeTranslatedConfigs(parent interface{}, parentTranslations translate.TranslationSet, child interface{}, childTranslations translate.TranslationSet) (interface{}, translate.TranslationSet) { + // mappings: + // left: parent or child translate.TranslationSet + // right: merge.Transcript + + // merge configs + result, right := merge.MergeStructTranscribe(parent, child) + + // merge left and right mappings into new TranslationSet + if parentTranslations.FromTag != childTranslations.FromTag || parentTranslations.ToTag != childTranslations.ToTag { + panic(fmt.Sprintf("mismatched translation tags, %s != %s || %s != %s", parentTranslations.FromTag, childTranslations.FromTag, parentTranslations.ToTag, childTranslations.ToTag)) + } + ts := translate.NewTranslationSet(parentTranslations.FromTag, parentTranslations.ToTag) + for _, rightEntry := range right.Mappings { + var left *translate.TranslationSet + switch rightEntry.From.Tag { + case merge.TAG_PARENT: + left = &parentTranslations + case merge.TAG_CHILD: + left = &childTranslations + default: + panic("unexpected mapping tag " + rightEntry.From.Tag) + } + leftEntry, ok := left.Set[rightEntry.From.String()] + if !ok { + // the right mapping is more comprehensive than the + // left mapping + continue + } + if _, ok := ts.Set[rightEntry.To.String()]; ok && rightEntry.From.Tag != merge.TAG_CHILD { + // For result fields which are produced by combining + // the parent and child, there will be two + // transcript entries, one for each side. We want + // to prefer the child because the parent is + // probably a desugared config whose source is + // textually unrelated to the result config. + // + // Currently, Ignition always reports parent before + // child, but that isn't necessarily contractual, so + // we don't assume it. Here, we've found the second + // entry and it's not from the child; skip it. + continue + } + rightEntry.To.Tag = leftEntry.To.Tag + ts.AddTranslation(leftEntry.From, rightEntry.To) + } + return result, ts +} diff --git a/butane/base/util/merge_test.go b/butane/base/util/merge_test.go new file mode 100644 index 000000000..f3c643e1d --- /dev/null +++ b/butane/base/util/merge_test.go @@ -0,0 +1,159 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + // config version doesn't matter; just pick one + "github.com/coreos/ignition/v2/config/v3_0/types" + "github.com/coreos/vcontext/path" + "github.com/stretchr/testify/assert" +) + +// TestMergeTranslatedConfigs tests merging two Ignition configs and their +// corresponding translations. +func TestMergeTranslatedConfigs(t *testing.T) { + tests := []struct { + parent types.Config + parentTranslations translate.TranslationSet + child types.Config + childTranslations translate.TranslationSet + merged types.Config + mergedTranslations translate.TranslationSet + }{ + { + parent: types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Name: "aardvark.service", + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("antelope"), + }, + { + Name: "caribou.service", + Contents: util.StrToPtr("caribou"), + }, + { + Name: "elephant.service", + Contents: util.StrToPtr("elephant"), + }, + }, + }, + }, + parentTranslations: makeTranslationSet([]translate.Translation{ + // parent key duplicated in child, should be clobbered + {From: path.New("in", "bad", 1), To: path.New("out", "systemd", "units", 0, "name")}, + // parent field overridden in child, should be clobbered + {From: path.New("in", "bad", 2), To: path.New("out", "systemd", "units", 0, "contents")}, + // parent field not overridden in child + {From: path.New("in", "good", 1), To: path.New("out", "systemd", "units", 0, "enabled")}, + // parent key not specified in child + {From: path.New("in", "good", 2), To: path.New("out", "systemd", "units", 1, "name")}, + // parent field not specified in child + {From: path.New("in", "good", 3), To: path.New("out", "systemd", "units", 1, "contents")}, + // other fields omitted from translation set + }), + child: types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Name: "bear.service", + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("bear"), + }, + { + Name: "aardvark.service", + Contents: util.StrToPtr("aardvark"), + }, + }, + }, + }, + childTranslations: makeTranslationSet([]translate.Translation{ + // child key not mentioned in parent + {From: path.New("in", "good", 11), To: path.New("out", "systemd", "units", 0, "name")}, + // child field not mentioned in parent + {From: path.New("in", "good", 12), To: path.New("out", "systemd", "units", 0, "contents")}, + // parent key duplicated in child + {From: path.New("in", "good", 13), To: path.New("out", "systemd", "units", 1, "name")}, + // parent field overridden in child + {From: path.New("in", "good", 14), To: path.New("out", "systemd", "units", 1, "contents")}, + // other fields omitted from translation set + }), + merged: types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Name: "aardvark.service", + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("aardvark"), + }, + { + Name: "caribou.service", + Contents: util.StrToPtr("caribou"), + }, + { + Name: "elephant.service", + Contents: util.StrToPtr("elephant"), + }, + { + Name: "bear.service", + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("bear"), + }, + }, + }, + }, + mergedTranslations: makeTranslationSet([]translate.Translation{ + {From: path.New("in", "good", 13), To: path.New("out", "systemd", "units", 0, "name")}, + {From: path.New("in", "good", 1), To: path.New("out", "systemd", "units", 0, "enabled")}, + {From: path.New("in", "good", 14), To: path.New("out", "systemd", "units", 0, "contents")}, + {From: path.New("in", "good", 2), To: path.New("out", "systemd", "units", 1, "name")}, + {From: path.New("in", "good", 3), To: path.New("out", "systemd", "units", 1, "contents")}, + {From: path.New("in", "good", 11), To: path.New("out", "systemd", "units", 3, "name")}, + {From: path.New("in", "good", 12), To: path.New("out", "systemd", "units", 3, "contents")}, + }), + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("merge %d", i), func(t *testing.T) { + c, ts := MergeTranslatedConfigs(test.parent, test.parentTranslations, test.child, test.childTranslations) + assert.Equal(t, test.merged, c, "bad config") + assert.Equal(t, test.mergedTranslations, ts, "bad translations") + }) + } +} + +func makeTranslationSet(translations []translate.Translation) translate.TranslationSet { + ts := translate.NewTranslationSet(translations[0].From.Tag, translations[0].To.Tag) + for _, t := range translations { + ts.AddTranslation(t.From, t.To) + } + return ts +} diff --git a/butane/base/util/test.go b/butane/base/util/test.go new file mode 100644 index 000000000..c34ab129a --- /dev/null +++ b/butane/base/util/test.go @@ -0,0 +1,145 @@ +// 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 util + +import ( + "reflect" + "regexp" + "strings" + "testing" + + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// helper functions for writing tests + +// VerifyTranslations validates a TranslationSet from 'yaml' to 'json'. It +// expects all translations to be identity, unless they match a listed one, +// and all the listed ones to exist. +func VerifyTranslations(t *testing.T, set translate.TranslationSet, exceptions []translate.Translation) { + // check tags + assert.Equal(t, set.FromTag, "yaml") + assert.Equal(t, set.ToTag, "json") + + // build up exceptions and check them + exceptionSet := translate.NewTranslationSet(set.FromTag, set.ToTag) + for _, ex := range exceptions { + exceptionSet.AddTranslation(ex.From, ex.To) + if tr, ok := set.Set[ex.To.String()]; ok { + assert.Equal(t, ex, tr, "non-identity translation with unexpected From") + } else { + t.Errorf("missing non-identity translation %v", ex) + } + } + + // walk translations + for key, translation := range set.Set { + // unexpected non-identity? + if _, ok := exceptionSet.Set[key]; !ok { + assert.Equal(t, translation.From.Path, translation.To.Path, "translation is not identity") + } + // camel case on left? + assert.NotRegexp(t, regexp.MustCompile("[A-Z]"), translation.From.String(), "from path in camelCase") + // snake case on right? + assert.NotContains(t, translation.To.String(), "_", "to path in snake_case") + } +} + +// VerifyReport verifies that every path in a report corresponds to a valid +// field in the object. +func VerifyReport(t *testing.T, obj interface{}, r report.Report) { + v := reflect.ValueOf(obj) + for _, entry := range r.Entries { + verifyPath(t, v, entry.Context) + } +} + +func verifyPath(t *testing.T, v reflect.Value, p path.ContextPath) { + if len(p.Path) == 0 { + return + } + switch v.Kind() { + case reflect.Map: + value := v.MapIndex(reflect.ValueOf(p.Path[0])) + if v.IsZero() { + t.Errorf("%s: path component %q is nonexistent map key", p, p.Path[0]) + return + } + verifyPath(t, value, p.Tail()) + case reflect.Pointer: + if !v.IsValid() || v.IsNil() { + t.Errorf("%s: path component %q points through a nil pointer", p, p.Path[0]) + return + } + verifyPath(t, v.Elem(), p) + case reflect.Slice: + index, ok := p.Path[0].(int) + if !ok { + t.Errorf("%s: path component %q is not a valid slice index", p, p.Path[0]) + return + } + if index >= v.Len() { + t.Errorf("%s: path index %d out of bounds for slice of length %d", p, index, v.Len()) + return + } + verifyPath(t, v.Index(index), p.Tail()) + case reflect.Struct: + fieldName, ok := p.Path[0].(string) + if !ok { + t.Errorf("%s: path component %q is not a valid struct field name", p, p.Path[0]) + return + } + if !verifyStruct(t, v, p, fieldName) { + t.Errorf("%s: path component %q refers to nonexistent field", p, p.Path[0]) + } + default: + t.Errorf("%s: path component %q points through kind %s", p, p.Path[0], v.Kind()) + } +} + +func verifyStruct(t *testing.T, v reflect.Value, p path.ContextPath, fieldName string) bool { + if v.Kind() != reflect.Struct { + panic("verifyStruct called on non-struct") + } + for i := 0; i < v.NumField(); i++ { + fieldType := v.Type().Field(i) + if fieldType.Anonymous { + if verifyStruct(t, v.Field(i), p, fieldName) { + return true + } + } else { + tag := strings.Split(fieldType.Tag.Get("yaml"), ",")[0] + if tag == fieldName { + verifyPath(t, v.Field(i), p.Tail()) + return true + } + } + } + // didn't find field + return false +} + +func CompressDataURL(t *testing.T, contents []byte) (string, string) { + t.Helper() + uri, compression, err := MakeDataURL(contents, nil, true) + if err != nil { + t.Fatalf("MakeDataURL: %v", err) + } + return uri, *compression +} diff --git a/butane/base/util/url.go b/butane/base/util/url.go new file mode 100644 index 000000000..98c33c1c4 --- /dev/null +++ b/butane/base/util/url.go @@ -0,0 +1,83 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "net/url" + + "github.com/coreos/ignition/v2/config/util" + "github.com/vincent-petithory/dataurl" +) + +func MakeDataURL(contents []byte, currentCompression *string, allowCompression bool) (uri string, compression *string, err error) { + // try three different encodings, and select the smallest one + + if util.NilOrEmpty(currentCompression) { + // The config does not specify compression. We need to + // explicitly set the compression field to avoid a child + // config inheriting a compression setting from the parent, + // which may not have used the same compression algorithm. + compression = util.StrToPtr("") + } else { + // The config specifies compression, meaning that the + // contents were compressed by the user, so we can pick a + // data URL encoding but we can't compress again. Return a + // nil compression value so the caller knows not to record a + // translation from input contents to output compression. + compression = nil + } + + // URL-escaped, useful for ASCII text + opaque := "," + dataurl.Escape(contents) + + // Base64-encoded, useful for small or incompressible binary data + b64 := ";base64," + base64.StdEncoding.EncodeToString(contents) + if len(b64) < len(opaque) { + opaque = b64 + } + + // Base64-encoded gzipped, useful for compressible data. If the + // user already enabled compression, don't compress again. + // We don't try base64-encoded URL-escaped because gzipped data is + // binary and URL escaping is unlikely to be efficient. + if util.NilOrEmpty(currentCompression) && allowCompression { + var buf bytes.Buffer + var compressor *gzip.Writer + if compressor, err = gzip.NewWriterLevel(&buf, gzip.BestCompression); err != nil { + return + } + if _, err = compressor.Write(contents); err != nil { + return + } + if err = compressor.Close(); err != nil { + return + } + gz := ";base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) + // Account for space needed by the compression value + if len(gz)+len("gzip") < len(opaque) { + opaque = gz + compression = util.StrToPtr("gzip") + } + } + + uri = (&url.URL{ + Scheme: "data", + Opaque: opaque, + }).String() + return +} diff --git a/butane/base/v0_1/schema.go b/butane/base/v0_1/schema.go new file mode 100644 index 000000000..bc3816524 --- /dev/null +++ b/butane/base/v0_1/schema.go @@ -0,0 +1,205 @@ +// Copyright 2019 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 v0_1 + +type CaReference struct { + Source string `yaml:"source"` + Verification Verification `yaml:"verification"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type ConfigReference struct { + Source *string `yaml:"source"` + Verification Verification `yaml:"verification"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []FileContents `yaml:"append"` + Contents FileContents `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type FileContents struct { + Compression *string `yaml:"compression"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + Options []FilesystemOption `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` +} + +type FilesystemOption string + +type Group string + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []ConfigReference `yaml:"merge"` + Replace ConfigReference `yaml:"replace"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target string `yaml:"target"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level string `yaml:"level"` + Name string `yaml:"name"` + Options []RaidOption `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type RaidOption string + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Raid []Raid `yaml:"raid"` +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type TLS struct { + CertificateAuthorities []CaReference `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_1/translate.go b/butane/base/v0_1/translate.go new file mode 100644 index 000000000..7062a20fb --- /dev/null +++ b/butane/base/v0_1/translate.go @@ -0,0 +1,111 @@ +// Copyright 2019 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 v0_1 + +import ( + "net/url" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_0/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/vincent-petithory/dataurl" +) + +// ToIgn3_0Unvalidated 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_0Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateFileContents) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateFileContents(from FileContents, options common.TranslateOptions) (to types.FileContents, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + if from.Inline != nil { + src := (&url.URL{ + Scheme: "data", + Opaque: "," + dataurl.EscapeString(*from.Inline), + }).String() + to.Source = &src + tm.AddTranslation(path.New("yaml", "inline"), path.New("json", "source")) + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} diff --git a/butane/base/v0_1/translate_test.go b/butane/base/v0_1/translate_test.go new file mode 100644 index 000000000..f12080616 --- /dev/null +++ b/butane/base/v0_1/translate_test.go @@ -0,0 +1,322 @@ +// Copyright 2019 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 v0_1 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_0/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + tests := []struct { + in File + out types.File + exceptions []translate.Translation + }{ + { + File{}, + types.File{}, + nil, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []FileContents{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: FileContents{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.FileContents{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + Contents: types.FileContents{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, 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") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.0.0", + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_0 tests the config.ToIgn3_0 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_0(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_0Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_1/validate.go b/butane/base/v0_1/validate.go new file mode 100644 index 000000000..2e978f94d --- /dev/null +++ b/butane/base/v0_1/validate.go @@ -0,0 +1,44 @@ +// Copyright 2019 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 v0_1 + +import ( + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (f FileContents) Validate(c path.ContextPath) (r report.Report) { + if f.Inline != nil && f.Source != nil { + r.AddOnError(c.Append("inline"), common.ErrTooManyResourceSources) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} diff --git a/butane/base/v0_1/validate_test.go b/butane/base/v0_1/validate_test.go new file mode 100644 index 000000000..01377f455 --- /dev/null +++ b/butane/base/v0_1/validate_test.go @@ -0,0 +1,151 @@ +// Copyright 2019 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 v0_1 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateFileContents tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateFileContents(t *testing.T) { + tests := []struct { + in FileContents + out error + }{ + {}, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + FileContents{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + }, + { + FileContents{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + }, + { + FileContents{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + }, + } + + 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{} + // hardcode inline for now since that's the only place errors occur. Move into the + // test struct once there's more than one place + expected.AddOnError(path.New("yaml", "inline"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} diff --git a/butane/base/v0_2/schema.go b/butane/base/v0_2/schema.go new file mode 100644 index 000000000..9a6ea30f7 --- /dev/null +++ b/butane/base/v0_2/schema.go @@ -0,0 +1,219 @@ +// Copyright 2019 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 v0_2 + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target string `yaml:"target"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level string `yaml:"level"` + Name string `yaml:"name"` + Options []RaidOption `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type RaidOption string + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Local string `yaml:"local"` + Path *string `yaml:"path"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_2/translate.go b/butane/base/v0_2/translate.go new file mode 100644 index 000000000..ac74a979e --- /dev/null +++ b/butane/base/v0_2/translate.go @@ -0,0 +1,374 @@ +// Copyright 2019 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 v0_2 + +import ( + "os" + slashpath "path" + "path/filepath" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_1/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(`# Generated by Butane +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- if .MountOptions }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} +{{- end }} + +[Install] +RequiredBy=local-fs.target`)) +) + +// ToIgn3_1Unvalidated 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_1Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + } + return ts, r +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + return nil + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if link.Target != "" { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = filepath.ToSlash(target) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + newUnit := mountUnitFromFS(fs) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + // unchecked deref of path ok, fs would fail validation otherwise + unitName := unit.UnitNamePathEscape(*fs.Path) + ".mount" + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_2/translate_test.go b/butane/base/v0_2/translate_test.go new file mode 100644 index 000000000..92d41c0cf --- /dev/null +++ b/butane/base/v0_2/translate_test.go @@ -0,0 +1,1627 @@ +// Copyright 2019 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 v0_2 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_1/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_1Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_1Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../nonexistent", + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: "file", + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_1Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.1.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.1.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.1.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.1.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_1 tests the config.ToIgn3_1 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_1(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_1Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_2/util.go b/butane/base/v0_2/util.go new file mode 100644 index 000000000..23593a685 --- /dev/null +++ b/butane/base/v0_2/util.go @@ -0,0 +1,158 @@ +// 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 v0_2 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_1/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_2/validate.go b/butane/base/v0_2/validate.go new file mode 100644 index 000000000..b1d552e8a --- /dev/null +++ b/butane/base/v0_2/validate.go @@ -0,0 +1,92 @@ +// Copyright 2019 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 v0_2 + +import ( + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} diff --git a/butane/base/v0_2/validate_test.go b/butane/base/v0_2/validate_test.go new file mode 100644 index 000000000..2a917b814 --- /dev/null +++ b/butane/base/v0_2/validate_test.go @@ -0,0 +1,253 @@ +// Copyright 2019 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 v0_2 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_3/schema.go b/butane/base/v0_3/schema.go new file mode 100644 index 000000000..507feb70a --- /dev/null +++ b/butane/base/v0_3/schema.go @@ -0,0 +1,254 @@ +// Copyright 2019 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 v0_3 + +type Clevis struct { + Custom *Custom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Custom struct { + Config string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin string `yaml:"pin"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target string `yaml:"target"` +} + +type Luks struct { + Clevis *Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + Options []LuksOption `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type LuksOption string + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level string `yaml:"level"` + Name string `yaml:"name"` + Options []RaidOption `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type RaidOption string + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Local string `yaml:"local"` + Path *string `yaml:"path"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_3/translate.go b/butane/base/v0_3/translate.go new file mode 100644 index 000000000..faa19a74b --- /dev/null +++ b/butane/base/v0_3/translate.go @@ -0,0 +1,397 @@ +// Copyright 2019 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 v0_3 + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(`# Generated by Butane +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} +{{- end }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }}`)) +) + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + } + return ts, r +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + return nil + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if link.Target != "" { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = filepath.ToSlash(target) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if luks.Clevis != nil && len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + // unchecked deref of path ok, fs would fail validation otherwise + unitName := unit.UnitNamePathEscape(*fs.Path) + ".mount" + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_3/translate_test.go b/butane/base/v0_3/translate_test.go new file mode 100644 index 000000000..45c304d6c --- /dev/null +++ b/butane/base/v0_3/translate_test.go @@ -0,0 +1,1781 @@ +// Copyright 2019 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 v0_3 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "/bar", + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_2Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: &Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: &types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: &Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: &types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_2Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../nonexistent", + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: "../file", + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: "file", + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_2Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.2.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.2.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.2.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.2.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_2 tests the config.ToIgn3_2 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_2(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_2Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_3/util.go b/butane/base/v0_3/util.go new file mode 100644 index 000000000..549cb8f3e --- /dev/null +++ b/butane/base/v0_3/util.go @@ -0,0 +1,158 @@ +// 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 v0_3 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_3/validate.go b/butane/base/v0_3/validate.go new file mode 100644 index 000000000..7628af700 --- /dev/null +++ b/butane/base/v0_3/validate.go @@ -0,0 +1,92 @@ +// Copyright 2019 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 v0_3 + +import ( + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} diff --git a/butane/base/v0_3/validate_test.go b/butane/base/v0_3/validate_test.go new file mode 100644 index 000000000..419ad8521 --- /dev/null +++ b/butane/base/v0_3/validate_test.go @@ -0,0 +1,253 @@ +// Copyright 2019 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 v0_3 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_4/schema.go b/butane/base/v0_4/schema.go new file mode 100644 index 000000000..9f5225413 --- /dev/null +++ b/butane/base/v0_4/schema.go @@ -0,0 +1,262 @@ +// 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 v0_4 + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + Options []LuksOption `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type LuksOption string + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []RaidOption `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type RaidOption string + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Local string `yaml:"local"` + Path *string `yaml:"path"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_4/translate.go b/butane/base/v0_4/translate.go new file mode 100644 index 000000000..106372e67 --- /dev/null +++ b/butane/base/v0_4/translate.go @@ -0,0 +1,420 @@ +// 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 v0_4 + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// ToIgn3_3Unvalidated 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_3Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + } + return ts, r +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + return nil + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_4/translate_test.go b/butane/base/v0_4/translate_test.go new file mode 100644 index 000000000..61fa6430c --- /dev/null +++ b/butane/base/v0_4/translate_test.go @@ -0,0 +1,1913 @@ +// 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 v0_4 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_3Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_3Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_3Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.3.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.3.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.3.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.3.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_3Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_3 tests the config.ToIgn3_3 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_3(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_3Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_4/util.go b/butane/base/v0_4/util.go new file mode 100644 index 000000000..9edac0d26 --- /dev/null +++ b/butane/base/v0_4/util.go @@ -0,0 +1,158 @@ +// 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 v0_4 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_4/validate.go b/butane/base/v0_4/validate.go new file mode 100644 index 000000000..229d6f0a9 --- /dev/null +++ b/butane/base/v0_4/validate.go @@ -0,0 +1,91 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v0_4 + +import ( + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} diff --git a/butane/base/v0_4/validate_test.go b/butane/base/v0_4/validate_test.go new file mode 100644 index 000000000..94acad5dc --- /dev/null +++ b/butane/base/v0_4/validate_test.go @@ -0,0 +1,320 @@ +// 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 v0_4 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + 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") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_5/schema.go b/butane/base/v0_5/schema.go new file mode 100644 index 000000000..09c88f3b3 --- /dev/null +++ b/butane/base/v0_5/schema.go @@ -0,0 +1,262 @@ +// 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 v0_5 + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + Discard *bool `yaml:"discard"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + OpenOptions []string `yaml:"open_options"` + Options []string `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []string `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` + Advertisement *string `yaml:"advertisement"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Local string `yaml:"local"` + Path *string `yaml:"path"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_5/translate.go b/butane/base/v0_5/translate.go new file mode 100644 index 000000000..b855b88c5 --- /dev/null +++ b/butane/base/v0_5/translate.go @@ -0,0 +1,510 @@ +// 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 v0_5 + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "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" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// 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) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir) + if err != nil { + r.AddOnError(c.Append(keyFileIndex), err) + continue + } + for _, line := range regexp.MustCompile("\r?\n").Split(string(sshKeys), -1) { + if line == "" { + continue + } + tm.AddTranslation(c.Append(keyFileIndex), path.New("json", "sshAuthorizedKeys", len(to.SSHAuthorizedKeys))) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateDropin) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + } + return ts, r +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + return nil + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_5/translate_test.go b/butane/base/v0_5/translate_test.go new file mode 100644 index 000000000..02072b33b --- /dev/null +++ b/butane/base/v0_5/translate_test.go @@ -0,0 +1,2367 @@ +// 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 v0_5 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_4Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.4.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.4.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.4.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.4.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_4Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLuks test translating the butane storage.luks.clevis.tang.[i] arguments to ignition storage.luks.clevis.tang.[i] entries. +func TestTranslateTang(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // Luks with tang and all options set, returns a valid ignition config with the same options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_4Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + report string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid inline keys", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + }, + "", + sshKeyDir, + }, + { + "valid multiple local key files", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName, sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid local and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid empty local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "", + sshKeyDir, + }, + { + "valid blank local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "missing local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(sshKeyDir, sshKeyNonExistingFileName) + ": " + osNotFound + "\n", + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(randomDir, sshKeyFileName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + report string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "non existing dropin contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty dropin contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_4 tests the config.ToIgn3_4 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_4(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_4Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_5/util.go b/butane/base/v0_5/util.go new file mode 100644 index 000000000..62112e5e1 --- /dev/null +++ b/butane/base/v0_5/util.go @@ -0,0 +1,158 @@ +// 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 v0_5 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_5/validate.go b/butane/base/v0_5/validate.go new file mode 100644 index 000000000..b115af3e6 --- /dev/null +++ b/butane/base/v0_5/validate.go @@ -0,0 +1,106 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v0_5 + +import ( + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} diff --git a/butane/base/v0_5/validate_test.go b/butane/base/v0_5/validate_test.go new file mode 100644 index 000000000..db0f76248 --- /dev/null +++ b/butane/base/v0_5/validate_test.go @@ -0,0 +1,412 @@ +// 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 v0_5 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + 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") + }) + } +} + +// TestValidateUnit tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_6/schema.go b/butane/base/v0_6/schema.go new file mode 100644 index 000000000..f95eed471 --- /dev/null +++ b/butane/base/v0_6/schema.go @@ -0,0 +1,267 @@ +// 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 v0_6 + +type Cex struct { + Enabled *bool `yaml:"enabled"` +} + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Cex Cex `yaml:"cex"` + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + Discard *bool `yaml:"discard"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + OpenOptions []string `yaml:"open_options"` + Options []string `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []string `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` + Advertisement *string `yaml:"advertisement"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Local string `yaml:"local"` + Path *string `yaml:"path"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_6/translate.go b/butane/base/v0_6/translate.go new file mode 100644 index 000000000..5df800e1f --- /dev/null +++ b/butane/base/v0_6/translate.go @@ -0,0 +1,510 @@ +// 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 v0_6 + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// ToIgn3_5Unvalidated 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_5Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir) + if err != nil { + r.AddOnError(c.Append(keyFileIndex), err) + continue + } + for _, line := range regexp.MustCompile("\r?\n").Split(string(sshKeys), -1) { + if line == "" { + continue + } + tm.AddTranslation(c.Append(keyFileIndex), path.New("json", "sshAuthorizedKeys", len(to.SSHAuthorizedKeys))) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateDropin) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, srcBaseDir, destBaseDir, options) + } + return ts, r +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, srcBaseDir, destBaseDir string, options common.TranslateOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + return nil + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: types.Node{ + Path: destPath, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_6/translate_test.go b/butane/base/v0_6/translate_test.go new file mode 100644 index 000000000..7c1076375 --- /dev/null +++ b/butane/base/v0_6/translate_test.go @@ -0,0 +1,2367 @@ +// 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 v0_6 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_5Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_5Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_5Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, []types.Directory(nil), actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.5.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.5.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.5.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.5.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_5Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLuks test translating the butane storage.luks.clevis.tang.[i] arguments to ignition storage.luks.clevis.tang.[i] entries. +func TestTranslateTang(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // Luks with tang and all options set, returns a valid ignition config with the same options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_5Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + report string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid inline keys", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + }, + "", + sshKeyDir, + }, + { + "valid multiple local key files", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName, sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid local and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid empty local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "", + sshKeyDir, + }, + { + "valid blank local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "missing local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(sshKeyDir, sshKeyNonExistingFileName) + ": " + osNotFound + "\n", + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(randomDir, sshKeyFileName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + report string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "non existing dropin contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty dropin contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_5 tests the config.ToIgn3_5 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_5(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_5Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_6/util.go b/butane/base/v0_6/util.go new file mode 100644 index 000000000..598b105c6 --- /dev/null +++ b/butane/base/v0_6/util.go @@ -0,0 +1,158 @@ +// 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 v0_6 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_6/validate.go b/butane/base/v0_6/validate.go new file mode 100644 index 000000000..925853011 --- /dev/null +++ b/butane/base/v0_6/validate.go @@ -0,0 +1,105 @@ +// 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 v0_6 + +import ( + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} diff --git a/butane/base/v0_6/validate_test.go b/butane/base/v0_6/validate_test.go new file mode 100644 index 000000000..6a51b2fc4 --- /dev/null +++ b/butane/base/v0_6/validate_test.go @@ -0,0 +1,412 @@ +// 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 v0_6 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + 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") + }) + } +} + +// TestValidateUnit tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_7/schema.go b/butane/base/v0_7/schema.go new file mode 100644 index 000000000..726bc4fa4 --- /dev/null +++ b/butane/base/v0_7/schema.go @@ -0,0 +1,271 @@ +// 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 v0_7 + +type Cex struct { + Enabled *bool `yaml:"enabled"` +} + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Cex Cex `yaml:"cex"` + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + Discard *bool `yaml:"discard"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + OpenOptions []string `yaml:"open_options"` + Options []string `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []string `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` + Advertisement *string `yaml:"advertisement"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Group NodeGroup `yaml:"group"` + Local string `yaml:"local"` + Path *string `yaml:"path"` + User NodeUser `yaml:"user"` + FileMode *int `yaml:"file_mode"` + DirMode *int `yaml:"dir_mode"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_7/translate.go b/butane/base/v0_7/translate.go new file mode 100644 index 000000000..388ff41dc --- /dev/null +++ b/butane/base/v0_7/translate.go @@ -0,0 +1,564 @@ +// 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 v0_7 + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "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" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// 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) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tm2, r2 := c.processTrees(&ret, options) + tm.Merge(tm2) + r.Merge(r2) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir) + if err != nil { + r.AddOnError(c.Append(keyFileIndex), err) + continue + } + for _, line := range regexp.MustCompile("\r?\n").Split(string(sshKeys), -1) { + if line == "" { + continue + } + tm.AddTranslation(c.Append(keyFileIndex), path.New("json", "sshAuthorizedKeys", len(to.SSHAuthorizedKeys))) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateDropin) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, treeWalkOptions{ + srcBaseDir: srcBaseDir, + destBaseDir: destBaseDir, + TranslateOptions: options, + user: tree.User, + group: tree.Group, + fileMode: tree.FileMode, + dirMode: tree.DirMode, + }) + } + return ts, r +} + +type treeWalkOptions struct { + srcBaseDir string + destBaseDir string + common.TranslateOptions + user NodeUser + group NodeGroup + fileMode *int + dirMode *int +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, options treeWalkOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(options.srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(options.srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(options.destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + // If nothing custom is required we skip directories generation + if options.dirMode == nil && options.user == (NodeUser{}) && options.group == (NodeGroup{}) { + return nil + } + + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + mode := util.IntToPtr(0755) + if options.dirMode != nil { + mode = options.dirMode + } + i, dir := t.AddDir(types.Directory{ + Node: createNode(destPath, options.user, options.group), + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: mode, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "directories", i), dir) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "directories")) + } + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + if options.fileMode != nil { + mode = *options.fileMode + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func createNode(destPath string, user NodeUser, group NodeGroup) types.Node { + return types.Node{ + Path: destPath, + User: types.NodeUser{ + ID: user.ID, + Name: user.Name, + }, + Group: types.NodeGroup{ + ID: group.ID, + Name: group.Name, + }, + } +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_7/translate_test.go b/butane/base/v0_7/translate_test.go new file mode 100644 index 000000000..193537661 --- /dev/null +++ b/butane/base/v0_7/translate_test.go @@ -0,0 +1,2522 @@ +// 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 v0_7 + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outDirs []types.Directory + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + // Permissions and ownership + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + { + Local: "tree2", + DirMode: util.IntToPtr(0777), + Path: util.StrToPtr("/etc"), + }, + }, + outDirs: []types.Directory{ + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/subdir", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Path: "/etc", + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0777), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // Overwrite via tree ownership fails + { + dirFiles: map[string]os.FileMode{ + "tree/etc/file": 0600, + }, + inDirs: []Directory{ + {Path: "/etc"}, + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_6Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, test.outDirs, actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.6.0", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.6.0", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.6.0", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.6.0", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_6Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLuks test translating the butane storage.luks.clevis.tang.[i] arguments to ignition storage.luks.clevis.tang.[i] entries. +func TestTranslateTang(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // Luks with tang and all options set, returns a valid ignition config with the same options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_6Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + report string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid inline keys", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + }, + "", + sshKeyDir, + }, + { + "valid multiple local key files", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName, sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid local and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid empty local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "", + sshKeyDir, + }, + { + "valid blank local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "missing local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(sshKeyDir, sshKeyNonExistingFileName) + ": " + osNotFound + "\n", + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(randomDir, sshKeyFileName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + report string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "non existing dropin contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty dropin contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_6 tests the config.ToIgn3_6 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_6(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_6Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/base/v0_7/util.go b/butane/base/v0_7/util.go new file mode 100644 index 000000000..0a60d0148 --- /dev/null +++ b/butane/base/v0_7/util.go @@ -0,0 +1,158 @@ +// 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 v0_7 + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_7/validate.go b/butane/base/v0_7/validate.go new file mode 100644 index 000000000..360c46823 --- /dev/null +++ b/butane/base/v0_7/validate.go @@ -0,0 +1,106 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v0_7 + +import ( + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + if rs.ContentsLocal != nil && rs.Contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} diff --git a/butane/base/v0_7/validate_test.go b/butane/base/v0_7/validate_test.go new file mode 100644 index 000000000..3de4670dd --- /dev/null +++ b/butane/base/v0_7/validate_test.go @@ -0,0 +1,412 @@ +// 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 v0_7 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + 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") + }) + } +} + +// TestValidateUnit tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/base/v0_8_exp/schema.go b/butane/base/v0_8_exp/schema.go new file mode 100644 index 000000000..c30a9987b --- /dev/null +++ b/butane/base/v0_8_exp/schema.go @@ -0,0 +1,280 @@ +// 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 v0_8_exp + +type Cex struct { + Enabled *bool `yaml:"enabled"` +} + +type Clevis struct { + Custom ClevisCustom `yaml:"custom"` + Tang []Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type ClevisCustom struct { + Config *string `yaml:"config"` + NeedsNetwork *bool `yaml:"needs_network"` + Pin *string `yaml:"pin"` +} + +type Config struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` + Ignition Ignition `yaml:"ignition"` + KernelArguments KernelArguments `yaml:"kernel_arguments"` + Passwd Passwd `yaml:"passwd"` + Storage Storage `yaml:"storage"` + Systemd Systemd `yaml:"systemd"` +} + +type Device string + +type Directory struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Mode *int `yaml:"mode"` +} + +type Disk struct { + Device string `yaml:"device"` + Partitions []Partition `yaml:"partitions"` + WipeTable *bool `yaml:"wipe_table"` +} + +type Dropin struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Name string `yaml:"name"` +} + +type File struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Append []Resource `yaml:"append"` + Contents Resource `yaml:"contents"` + Mode *int `yaml:"mode"` +} + +type Filesystem struct { + Device string `yaml:"device"` + Format *string `yaml:"format"` + Label *string `yaml:"label"` + MountOptions []string `yaml:"mount_options"` + Options []string `yaml:"options"` + Path *string `yaml:"path"` + UUID *string `yaml:"uuid"` + WipeFilesystem *bool `yaml:"wipe_filesystem"` + WithMountUnit *bool `yaml:"with_mount_unit" butane:"auto_skip"` // Added, not in Ignition spec +} + +type Group string + +type HTTPHeader struct { + Name string `yaml:"name"` + Value *string `yaml:"value"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `yaml:"config"` + Proxy Proxy `yaml:"proxy"` + Security Security `yaml:"security"` + Timeouts Timeouts `yaml:"timeouts"` +} + +type IgnitionConfig struct { + Merge []Resource `yaml:"merge"` + Replace Resource `yaml:"replace"` +} + +type KernelArgument string + +type KernelArguments struct { + ShouldExist []KernelArgument `yaml:"should_exist"` + ShouldNotExist []KernelArgument `yaml:"should_not_exist"` +} + +type Link struct { + Group NodeGroup `yaml:"group"` + Overwrite *bool `yaml:"overwrite"` + Path string `yaml:"path"` + User NodeUser `yaml:"user"` + Hard *bool `yaml:"hard"` + Target *string `yaml:"target"` +} + +type Luks struct { + Cex Cex `yaml:"cex"` + Clevis Clevis `yaml:"clevis"` + Device *string `yaml:"device"` + Discard *bool `yaml:"discard"` + KeyFile Resource `yaml:"key_file"` + Label *string `yaml:"label"` + Name string `yaml:"name"` + OpenOptions []string `yaml:"open_options"` + Options []string `yaml:"options"` + UUID *string `yaml:"uuid"` + WipeVolume *bool `yaml:"wipe_volume"` +} + +type NodeGroup struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type NodeUser struct { + ID *int `yaml:"id"` + Name *string `yaml:"name"` +} + +type Partition struct { + GUID *string `yaml:"guid"` + Label *string `yaml:"label"` + Number int `yaml:"number"` + Resize *bool `yaml:"resize"` + ShouldExist *bool `yaml:"should_exist"` + SizeMiB *int `yaml:"size_mib"` + StartMiB *int `yaml:"start_mib"` + TypeGUID *string `yaml:"type_guid"` + WipePartitionEntry *bool `yaml:"wipe_partition_entry"` +} + +type Passwd struct { + Groups []PasswdGroup `yaml:"groups"` + Users []PasswdUser `yaml:"users"` +} + +type PasswdGroup struct { + Gid *int `yaml:"gid"` + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` + ShouldExist *bool `yaml:"should_exist"` + System *bool `yaml:"system"` +} + +type PasswdUser struct { + Gecos *string `yaml:"gecos"` + Groups []Group `yaml:"groups"` + HomeDir *string `yaml:"home_dir"` + Name string `yaml:"name"` + NoCreateHome *bool `yaml:"no_create_home"` + NoLogInit *bool `yaml:"no_log_init"` + NoUserGroup *bool `yaml:"no_user_group"` + PasswordHash *string `yaml:"password_hash"` + PrimaryGroup *string `yaml:"primary_group"` + ShouldExist *bool `yaml:"should_exist"` + SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"` + SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"` + Shell *string `yaml:"shell"` + System *bool `yaml:"system"` + UID *int `yaml:"uid"` +} + +type Proxy struct { + HTTPProxy *string `yaml:"http_proxy"` + HTTPSProxy *string `yaml:"https_proxy"` + NoProxy []string `yaml:"no_proxy"` +} + +type Raid struct { + Devices []Device `yaml:"devices"` + Level *string `yaml:"level"` + Name string `yaml:"name"` + Options []string `yaml:"options"` + Spares *int `yaml:"spares"` +} + +type Resource struct { + Compression *string `yaml:"compression"` + HTTPHeaders HTTPHeaders `yaml:"http_headers"` + Source *string `yaml:"source"` + Inline *string `yaml:"inline"` // Added, not in ignition spec + Local *string `yaml:"local"` // Added, not in ignition spec + Verification Verification `yaml:"verification"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `yaml:"tls"` +} + +type Storage struct { + Directories []Directory `yaml:"directories"` + Disks []Disk `yaml:"disks"` + Files []File `yaml:"files"` + Filesystems []Filesystem `yaml:"filesystems"` + Links []Link `yaml:"links"` + Luks []Luks `yaml:"luks"` + Raid []Raid `yaml:"raid"` + Trees []Tree `yaml:"trees" butane:"auto_skip"` // Added, not in ignition spec +} + +type Systemd struct { + Units []Unit `yaml:"units"` + Quadlets []Quadlet `yaml:"quadlets" butane:"auto_skip"` // Added, not in ignition spec +} + +type Tang struct { + Thumbprint *string `yaml:"thumbprint"` + URL string `yaml:"url"` + Advertisement *string `yaml:"advertisement"` +} + +type TLS struct { + CertificateAuthorities []Resource `yaml:"certificate_authorities"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `yaml:"http_response_headers"` + HTTPTotal *int `yaml:"http_total"` +} + +type Tree struct { + Group NodeGroup `yaml:"group"` + Local string `yaml:"local"` + Path *string `yaml:"path"` + User NodeUser `yaml:"user"` + FileMode *int `yaml:"file_mode"` + DirMode *int `yaml:"dir_mode"` +} + +type Unit struct { + Contents *string `yaml:"contents"` + ContentsLocal *string `yaml:"contents_local"` + Dropins []Dropin `yaml:"dropins"` + Enabled *bool `yaml:"enabled"` + Mask *bool `yaml:"mask"` + Name string `yaml:"name"` +} + +type Quadlet struct { + Contents *string `yaml:"contents"` // file contents + ContentsLocal *string `yaml:"contents_local"` // path to the file + Name string `yaml:"name"` + Rootful bool `yaml:"rootful,omitempty"` + Dropins []Dropin `yaml:"dropins,omitempty"` +} + +type Verification struct { + Hash *string `yaml:"hash"` +} diff --git a/butane/base/v0_8_exp/translate.go b/butane/base/v0_8_exp/translate.go new file mode 100644 index 000000000..06c78ae95 --- /dev/null +++ b/butane/base/v0_8_exp/translate.go @@ -0,0 +1,734 @@ +// 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 v0_8_exp + +import ( + "fmt" + "os" + slashpath "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/go-systemd/v22/unit" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +var ( + mountUnitTemplate = template.Must(template.New("unit").Parse(` +{{- define "options" }} + {{- if or .MountOptions .Remote }} +Options= + {{- range $i, $opt := .MountOptions }} + {{- if $i }},{{ end }} + {{- $opt }} + {{- end }} + {{- if .Remote }}{{ if .MountOptions }},{{ end }}_netdev{{ end }} + {{- end }} +{{- end -}} + +# Generated by Butane +{{- if .Swap }} +[Swap] +What={{.Device}} +{{- template "options" . }} + +[Install] +RequiredBy=swap.target +{{- else }} +[Unit] +Requires=systemd-fsck@{{.EscapedDevice}}.service +After=systemd-fsck@{{.EscapedDevice}}.service + +[Mount] +Where={{.Path}} +What={{.Device}} +Type={{.Format}} +{{- template "options" . }} + +[Install] +{{- if .Remote }} +RequiredBy=remote-fs.target +{{- else }} +RequiredBy=local-fs.target +{{- end }} +{{- end }}`)) +) + +// ToIgn3_7Unvalidated 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_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret := types.Config{} + + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateIgnition) + tr.AddCustomTranslator(translateFile) + tr.AddCustomTranslator(translateDirectory) + tr.AddCustomTranslator(translateLink) + tr.AddCustomTranslator(translateResource) + tr.AddCustomTranslator(translatePasswdUser) + tr.AddCustomTranslator(translateUnit) + + tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition) + tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version")) + tm.AddTranslation(path.New("yaml", "ignition"), path.New("json", "ignition")) + translate.MergeP2(tr, tm, &r, "kernel_arguments", &c.KernelArguments, "kernelArguments", &ret.KernelArguments) + translate.MergeP(tr, tm, &r, "passwd", &c.Passwd, &ret.Passwd) + translate.MergeP(tr, tm, &r, "storage", &c.Storage, &ret.Storage) + translate.MergeP(tr, tm, &r, "systemd", &c.Systemd, &ret.Systemd) + + c.addMountUnits(&ret, &tm) + + tmTrees, rTrees := c.processTrees(&ret, options) + tmQuadlets, rQuadlets := c.processQuadlets(&ret, options) + + tm.Merge(tmTrees) + tm.Merge(tmQuadlets) + r.Merge(rTrees) + r.Merge(rQuadlets) + + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + return ret, tm, r +} + +func translateIgnition(from Ignition, options common.TranslateOptions) (to types.Ignition, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + to.Version = types.MaxVersion.String() + tm, r = translate.Prefixed(tr, "config", &from.Config, &to.Config) + translate.MergeP(tr, tm, &r, "proxy", &from.Proxy, &to.Proxy) + translate.MergeP(tr, tm, &r, "security", &from.Security, &to.Security) + translate.MergeP(tr, tm, &r, "timeouts", &from.Timeouts, &to.Timeouts) + return +} + +func translateFile(from File, options common.TranslateOptions) (to types.File, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateResource) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "append", &from.Append, &to.Append) + translate.MergeP(tr, tm, &r, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateResource(from Resource, options common.TranslateOptions) (to types.Resource, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "verification", &from.Verification, &to.Verification) + translate.MergeP2(tr, tm, &r, "http_headers", &from.HTTPHeaders, "httpHeaders", &to.HTTPHeaders) + translate.MergeP(tr, tm, &r, "source", &from.Source, &to.Source) + translate.MergeP(tr, tm, &r, "compression", &from.Compression, &to.Compression) + + if from.Local != nil { + c := path.New("yaml", "local") + contents, err := baseutil.ReadLocalFile(*from.Local, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + // Validating the contents of the local file from here since there is no way to + // get both the filename and filedirectory in the Validate context + if strings.HasPrefix(c.String(), "$.ignition.config") { + rp, err := ValidateIgnitionConfig(c, contents) + r.Merge(rp) + if err != nil { + return + } + } + + src, compression, err := baseutil.MakeDataURL(contents, to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + + if from.Inline != nil { + c := path.New("yaml", "inline") + + src, compression, err := baseutil.MakeDataURL([]byte(*from.Inline), to.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(c, err) + return + } + to.Source = &src + tm.AddTranslation(c, path.New("json", "source")) + if compression != nil { + to.Compression = compression + tm.AddTranslation(c, path.New("json", "compression")) + } + } + return +} + +func translateDirectory(from Directory, options common.TranslateOptions) (to types.Directory, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + translate.MergeP(tr, tm, &r, "mode", &from.Mode, &to.Mode) + return +} + +func translateLink(from Link, options common.TranslateOptions) (to types.Link, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "group", &from.Group, &to.Group) + translate.MergeP(tr, tm, &r, "user", &from.User, &to.User) + translate.MergeP(tr, tm, &r, "target", &from.Target, &to.Target) + translate.MergeP(tr, tm, &r, "hard", &from.Hard, &to.Hard) + translate.MergeP(tr, tm, &r, "overwrite", &from.Overwrite, &to.Overwrite) + translate.MergeP(tr, tm, &r, "path", &from.Path, &to.Path) + return +} + +func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos) + translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups) + translate.MergeP2(tr, tm, &r, "home_dir", &from.HomeDir, "homeDir", &to.HomeDir) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + translate.MergeP2(tr, tm, &r, "no_create_home", &from.NoCreateHome, "noCreateHome", &to.NoCreateHome) + translate.MergeP2(tr, tm, &r, "no_log_init", &from.NoLogInit, "noLogInit", &to.NoLogInit) + translate.MergeP2(tr, tm, &r, "no_user_group", &from.NoUserGroup, "noUserGroup", &to.NoUserGroup) + translate.MergeP2(tr, tm, &r, "password_hash", &from.PasswordHash, "passwordHash", &to.PasswordHash) + translate.MergeP2(tr, tm, &r, "primary_group", &from.PrimaryGroup, "primaryGroup", &to.PrimaryGroup) + translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell) + translate.MergeP2(tr, tm, &r, "should_exist", &from.ShouldExist, "shouldExist", &to.ShouldExist) + translate.MergeP2(tr, tm, &r, "ssh_authorized_keys", &from.SSHAuthorizedKeys, "sshAuthorizedKeys", &to.SSHAuthorizedKeys) + translate.MergeP(tr, tm, &r, "system", &from.System, &to.System) + translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID) + + if len(from.SSHAuthorizedKeysLocal) > 0 { + c := path.New("yaml", "ssh_authorized_keys_local") + tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys")) + + if options.FilesDir == "" { + r.AddOnError(c, common.ErrNoFilesDir) + return + } + + for keyFileIndex, sshKeyFile := range from.SSHAuthorizedKeysLocal { + sshKeys, err := baseutil.ReadLocalFile(sshKeyFile, options.FilesDir) + if err != nil { + r.AddOnError(c.Append(keyFileIndex), err) + continue + } + for _, line := range regexp.MustCompile("\r?\n").Split(string(sshKeys), -1) { + if line == "" { + continue + } + tm.AddTranslation(c.Append(keyFileIndex), path.New("json", "sshAuthorizedKeys", len(to.SSHAuthorizedKeys))) + to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(line)) + } + } + } + + return +} + +func translateUnit(from Unit, options common.TranslateOptions) (to types.Unit, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tr.AddCustomTranslator(translateDropin) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "dropins", &from.Dropins, &to.Dropins) + translate.MergeP(tr, tm, &r, "enabled", &from.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "mask", &from.Mask, &to.Mask) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dropin, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "contents", &from.Contents, &to.Contents) + translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name) + + if util.NotEmpty(from.ContentsLocal) { + c := path.New("yaml", "contents_local") + contents, err := baseutil.ReadLocalFile(*from.ContentsLocal, options.FilesDir) + if err != nil { + r.AddOnError(c, err) + return + } + tm.AddTranslation(c, path.New("json", "contents")) + to.Contents = util.StrToPtr(string(contents)) + } + + return +} + +// buildQuadletPath returns the filesystem path for a quadlet. +// See https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html +func buildQuadletPath(isRoot bool, quadletName string) string { + const ( + adminContainersPath = "/etc/containers/systemd" + userContainersPath = "/etc/containers/systemd/users" + ) + var base string + if isRoot { + base = adminContainersPath + } else { + base = userContainersPath + } + return slashpath.Join(base, quadletName) +} + +// isTemplateInstance checks if a quadlet name is a template instance (e.g. foo@100.container). +// Returns true and the base template name (e.g. foo@.container) if it is an instance. +func isTemplateInstance(name string) (bool, string) { + splitIndex := strings.Index(name, "@") + if splitIndex == -1 { + return false, "" + } + extensionIndex := strings.LastIndex(name, ".") + if extensionIndex == -1 || splitIndex+1 == extensionIndex { + return false, "" + } + baseName := name[:splitIndex] + extension := name[extensionIndex+1:] + templateName := fmt.Sprintf("%s@.%s", baseName, extension) + return true, templateName +} + +// readLocalOrInlineContents reads content from either a local file or inline string (see Quadlet and Dropin). +// Returns the content as bytes, the source path for error reporting, and any errors. +func readLocalOrInlineContents(contentsLocal, contentsInline *string, ctxPath path.ContextPath, options common.TranslateOptions) (content []byte, contentPath path.ContextPath, err error) { + if util.NotEmpty(contentsLocal) { + contentPath = ctxPath.Append("contents_local") + localContents, err := baseutil.ReadLocalFile(*contentsLocal, options.FilesDir) + if err != nil { + return content, contentPath, err + } + content = localContents + } + + if util.NotEmpty(contentsInline) { + contentPath = ctxPath.Append("contents") + content = []byte(*contentsInline) + } + return +} + +// addFileWithContents reads content (local or inline) and creates a file node in the tracker. +// Used for both quadlet files and their drop-ins, both of which will have either contentsLocal, or contents, but not both. +func addFileWithContents( + contentsLocal, inlineContents *string, + destPath string, + ctxPath path.ContextPath, + t *nodeTracker, + options common.TranslateOptions, +) (translate.TranslationSet, report.Report) { + var r report.Report + ts := translate.NewTranslationSet("yaml", "json") + + _, file := t.GetFile(destPath) + // If the node already exists, we dont want to over-write, we will just error + if (file != nil && util.NotEmpty(file.Contents.Source)) || t.Exists(destPath) { + r.AddOnError(ctxPath, common.ErrNodeExists) + return ts, r + } + + i, file := t.AddFile(types.File{Node: createNode(destPath, NodeUser{}, NodeGroup{})}) + if i == 0 { + ts.AddTranslation(ctxPath, path.New("json", "storage", "files")) + } + ts.AddFromCommonSource(ctxPath, path.New("json", "storage", "files", i), file) + ts.AddTranslation(ctxPath.Append("name"), path.New("json", "storage", "files", i, "path")) + contentBytes, contentPath, err := readLocalOrInlineContents(contentsLocal, inlineContents, ctxPath, options) + if err != nil { + r.AddOnError(contentPath, err) + return ts, r + } + url, compression, err := baseutil.MakeDataURL(contentBytes, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(ctxPath, err) + return ts, r + } + file.Contents.Source = &url + ts.AddTranslation(contentPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(ctxPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(contentPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + file.Mode = &mode + ts.AddTranslation(ctxPath, path.New("json", "storage", "files", i, "mode")) + } + + return ts, r +} + +// quadletToSymlink creates a symlink node for a template instance pointing to its base template. +func quadletToSymlink(quadlet Quadlet, quadletPath path.ContextPath, t *nodeTracker, templateName string) (translate.TranslationSet, report.Report) { + var r report.Report + ts := translate.NewTranslationSet("yaml", "json") + + destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) + _, link := t.GetLink(destPath) + // If the node already exists, we don't want to over-write, we will just error + if link != nil || t.Exists(destPath) { + r.AddOnError(quadletPath, common.ErrNodeExists) + return ts, r + } + + i, link := t.AddLink(types.Link{Node: types.Node{Path: destPath}, LinkEmbedded1: types.LinkEmbedded1{ + Target: &templateName, + }}) + if i == 0 { + ts.AddTranslation(quadletPath, path.New("json", "storage", "links")) + } + ts.AddFromCommonSource(quadletPath, path.New("json", "storage", "links", i), link) + ts.AddTranslation(quadletPath.Append("name"), path.New("json", "storage", "links", i, "path")) + ts.AddTranslation(quadletPath, path.New("json", "storage", "links", i, "target")) + return ts, r +} + +func (c Config) processQuadlets(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Systemd.Quadlets) == 0 { + return ts, r + } + + t := newNodeTracker(ret) + quadletsPath := path.New("yaml", "systemd", "quadlets") + ts.AddTranslation(quadletsPath, path.New("json", "storage")) // quadlets will be translated to storage (files and links) + for quadletNum, quadlet := range c.Systemd.Quadlets { + quadletPath := quadletsPath.Append(quadletNum) + + // We need to handle `foo@bar.container` differently than `foo@.container`, as the former needs to be a symlink to the latter + var tsFile translate.TranslationSet + var rFile report.Report + if isTemplate, templateName := isTemplateInstance(quadlet.Name); isTemplate { + tsFile, rFile = quadletToSymlink(quadlet, quadletPath, t, templateName) + } else { + destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) + tsFile, rFile = addFileWithContents(quadlet.ContentsLocal, quadlet.Contents, destPath, quadletPath, t, options) + } + + ts.Merge(tsFile) + r.Merge(rFile) + + for i, dropin := range quadlet.Dropins { + dropinPath := quadletPath.Append("dropins").Append(i) + destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) + ".d/" + dropin.Name + tsFile, rFile := addFileWithContents(dropin.ContentsLocal, dropin.Contents, destPath, dropinPath, t, options) + ts.Merge(tsFile) + r.Merge(rFile) + } + } + + return ts, r +} + +func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + if len(c.Storage.Trees) == 0 { + return ts, r + } + t := newNodeTracker(ret) + + for i, tree := range c.Storage.Trees { + yamlPath := path.New("yaml", "storage", "trees", i) + if options.FilesDir == "" { + r.AddOnError(yamlPath, common.ErrNoFilesDir) + return ts, r + } + + // calculate base path within FilesDir and check for + // path traversal + srcBaseDir := filepath.Join(options.FilesDir, filepath.FromSlash(tree.Local)) + if err := baseutil.EnsurePathWithinFilesDir(srcBaseDir, options.FilesDir); err != nil { + r.AddOnError(yamlPath, err) + continue + } + info, err := os.Stat(srcBaseDir) + if err != nil { + r.AddOnError(yamlPath, err) + continue + } + if !info.IsDir() { + r.AddOnError(yamlPath, common.ErrTreeNotDirectory) + continue + } + destBaseDir := "/" + if util.NotEmpty(tree.Path) { + destBaseDir = *tree.Path + } + + walkTree(yamlPath, &ts, &r, t, treeWalkOptions{ + srcBaseDir: srcBaseDir, + destBaseDir: destBaseDir, + TranslateOptions: options, + user: tree.User, + group: tree.Group, + fileMode: tree.FileMode, + dirMode: tree.DirMode, + }) + } + return ts, r +} + +type treeWalkOptions struct { + srcBaseDir string + destBaseDir string + common.TranslateOptions + user NodeUser + group NodeGroup + fileMode *int + dirMode *int +} + +func walkTree(yamlPath path.ContextPath, ts *translate.TranslationSet, r *report.Report, t *nodeTracker, options treeWalkOptions) { + // The strategy for errors within WalkFunc is to add an error to + // the report and return nil, so walking continues but translation + // will fail afterward. + err := filepath.Walk(options.srcBaseDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + relPath, err := filepath.Rel(options.srcBaseDir, srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + destPath := slashpath.Join(options.destBaseDir, filepath.ToSlash(relPath)) + + if info.Mode().IsDir() { + // If nothing custom is required we skip directories generation + if options.dirMode == nil && options.user == (NodeUser{}) && options.group == (NodeGroup{}) { + return nil + } + + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + mode := util.IntToPtr(0755) + if options.dirMode != nil { + mode = options.dirMode + } + i, dir := t.AddDir(types.Directory{ + Node: createNode(destPath, options.user, options.group), + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: mode, + }, + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "directories", i), dir) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "directories")) + } + } else if info.Mode().IsRegular() { + i, file := t.GetFile(destPath) + if file != nil { + if util.NotEmpty(file.Contents.Source) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, file = t.AddFile(types.File{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "files", i), file) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "files")) + } + } + contents, err := os.ReadFile(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + url, compression, err := baseutil.MakeDataURL(contents, file.Contents.Compression, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + file.Contents.Source = &url + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "source")) + if compression != nil { + file.Contents.Compression = compression + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents", "compression")) + } + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "contents")) + if file.Mode == nil { + mode := 0644 + if info.Mode()&0111 != 0 { + mode = 0755 + } + if options.fileMode != nil { + mode = *options.fileMode + } + file.Mode = &mode + ts.AddTranslation(yamlPath, path.New("json", "storage", "files", i, "mode")) + } + } else if info.Mode()&os.ModeType == os.ModeSymlink { + i, link := t.GetLink(destPath) + if link != nil { + if util.NotEmpty(link.Target) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + } else { + if t.Exists(destPath) { + r.AddOnError(yamlPath, common.ErrNodeExists) + return nil + } + i, link = t.AddLink(types.Link{ + Node: createNode(destPath, options.user, options.group), + }) + ts.AddFromCommonSource(yamlPath, path.New("json", "storage", "links", i), link) + if i == 0 { + ts.AddTranslation(yamlPath, path.New("json", "storage", "links")) + } + } + target, err := os.Readlink(srcPath) + if err != nil { + r.AddOnError(yamlPath, err) + return nil + } + link.Target = util.StrToPtr(filepath.ToSlash(target)) + ts.AddTranslation(yamlPath, path.New("json", "storage", "links", i, "target")) + } else { + r.AddOnError(yamlPath, common.ErrFileType) + return nil + } + return nil + }) + r.AddOnError(yamlPath, err) +} + +func createNode(destPath string, user NodeUser, group NodeGroup) types.Node { + return types.Node{ + Path: destPath, + User: types.NodeUser{ + ID: user.ID, + Name: user.Name, + }, + Group: types.NodeGroup{ + ID: group.ID, + Name: group.Name, + }, + } +} + +func (c Config) addMountUnits(config *types.Config, ts *translate.TranslationSet) { + if len(c.Storage.Filesystems) == 0 { + return + } + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd")) + renderedTranslations.AddTranslation(path.New("yaml", "storage", "filesystems"), path.New("json", "systemd", "units")) + for i, fs := range c.Storage.Filesystems { + if !util.IsTrue(fs.WithMountUnit) { + continue + } + fromPath := path.New("yaml", "storage", "filesystems", i, "with_mount_unit") + remote := false + // check filesystems targeting /dev/mapper devices against LUKS to determine if a + // remote mount is needed + if strings.HasPrefix(fs.Device, "/dev/mapper/") || strings.HasPrefix(fs.Device, "/dev/disk/by-id/dm-name-") { + for _, luks := range c.Storage.Luks { + // LUKS devices are opened with their name specified + if fs.Device == fmt.Sprintf("/dev/mapper/%s", luks.Name) || fs.Device == fmt.Sprintf("/dev/disk/by-id/dm-name-%s", luks.Name) { + if len(luks.Clevis.Tang) > 0 { + remote = true + break + } + } + } + } + newUnit := mountUnitFromFS(fs, remote) + unitPath := path.New("json", "systemd", "units", len(rendered.Systemd.Units)) + rendered.Systemd.Units = append(rendered.Systemd.Units, newUnit) + renderedTranslations.AddFromCommonSource(fromPath, unitPath, newUnit) + } + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations +} + +func mountUnitFromFS(fs Filesystem, remote bool) types.Unit { + context := struct { + *Filesystem + EscapedDevice string + Remote bool + Swap bool + }{ + Filesystem: &fs, + EscapedDevice: unit.UnitNamePathEscape(fs.Device), + Remote: remote, + // unchecked deref of format ok, fs would fail validation otherwise + Swap: *fs.Format == "swap", + } + contents := strings.Builder{} + err := mountUnitTemplate.Execute(&contents, context) + if err != nil { + panic(err) + } + var unitName string + if context.Swap { + unitName = unit.UnitNamePathEscape(fs.Device) + ".swap" + } else { + // unchecked deref of path ok, fs would fail validation otherwise + unitName = unit.UnitNamePathEscape(*fs.Path) + ".mount" + } + return types.Unit{ + Name: unitName, + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(contents.String()), + } +} diff --git a/butane/base/v0_8_exp/translate_test.go b/butane/base/v0_8_exp/translate_test.go new file mode 100644 index 000000000..16aa6dead --- /dev/null +++ b/butane/base/v0_8_exp/translate_test.go @@ -0,0 +1,2909 @@ +// 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 v0_8_exp + +import ( + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +var ( + osStatName string + osNotFound string +) + +func init() { + if runtime.GOOS == "windows" { + osStatName = "GetFileAttributesEx" + osNotFound = "The system cannot find the file specified." + } else { + osStatName = "stat" + osNotFound = "no such file or directory" + } +} + +// TestTranslateFile tests translating the ct storage.files.[i] entries to ignition storage.files.[i] entries. +func TestTranslateFile(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + zzzURI, zzzCompression := baseutil.CompressDataURL(t, []byte(zzz)) + random := "\xc0\x9cl\x01\x89i\xa5\xbfW\xe4\x1b\xf4J_\xb79P\xa3#\xa7" + randomURI, randomCompression := baseutil.CompressDataURL(t, []byte(random)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "file-1": "file contents\n", + "file-2": zzz, + "file-3": random, + "subdir/file-4": "subdir file contents\n", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + in File + out types.File + exceptions []translate.Translation + report string + options common.TranslateOptions + }{ + { + File{}, + types.File{}, + nil, + "", + common.TranslateOptions{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + File{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Append: []Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Local: util.StrToPtr("file-1"), + }, + }, + Overwrite: util.BoolToPtr(true), + Contents: Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: HTTPHeaders{ + HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + FileEmbedded1: types.FileEmbedded1{ + Mode: util.IntToPtr(420), + Append: []types.Resource{ + { + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,hello"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + { + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + Contents: types.Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + HTTPHeaders: types.HTTPHeaders{ + types.HTTPHeader{ + Name: "Header", + Value: util.StrToPtr("this isn't validated"), + }, + }, + Verification: types.Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "append", 0, "http_headers"), + To: path.New("json", "append", 0, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0), + To: path.New("json", "append", 0, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "name"), + To: path.New("json", "append", 0, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 0, "http_headers", 0, "value"), + To: path.New("json", "append", 0, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "http_headers"), + To: path.New("json", "append", 1, "httpHeaders"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0), + To: path.New("json", "append", 1, "httpHeaders", 0), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "name"), + To: path.New("json", "append", 1, "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "append", 1, "http_headers", 0, "value"), + To: path.New("json", "append", 1, "httpHeaders", 0, "value"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "contents", "http_headers"), + To: path.New("json", "contents", "httpHeaders"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0), + To: path.New("json", "contents", "httpHeaders", 0), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "name"), + To: path.New("json", "contents", "httpHeaders", 0, "name"), + }, + { + From: path.New("yaml", "contents", "http_headers", 0, "value"), + To: path.New("json", "contents", "httpHeaders", 0, "value"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline file contents + { + File{ + Path: "/foo", + Contents: Resource{ + // String is too short for auto gzip compression + Inline: util.StrToPtr("xyzzy"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{}, + }, + // local file contents + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // local file in subdirectory + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("subdir/file-4"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,subdir%20file%20contents%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "local"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // filesDir not specified + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrNoFilesDir.Error() + "\n", + common.TranslateOptions{}, + }, + // attempted directory traversal + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("../file-1"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: " + common.ErrFilesDirEscape.Error() + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // attempted inclusion of nonexistent file + { + File{ + Path: "/foo", + Contents: Resource{ + Local: util.StrToPtr("file-missing"), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + }, + []translate.Translation{}, + "error at $.contents.local: open " + filepath.Join(filesDir, "file-missing") + ": " + osNotFound + "\n", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // inline and local automatic file encoding + { + File{ + Path: "/foo", + Contents: Resource{ + // gzip + Inline: util.StrToPtr(zzz), + }, + Append: []Resource{ + { + // gzip + Local: util.StrToPtr("file-2"), + }, + { + // base64 + Inline: util.StrToPtr(random), + }, + { + // base64 + Local: util.StrToPtr("file-3"), + }, + { + // URL-escaped + Inline: util.StrToPtr(zzz), + Compression: util.StrToPtr("invalid"), + }, + { + // URL-escaped + Local: util.StrToPtr("file-2"), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + Append: []types.Resource{ + { + Source: util.StrToPtr(zzzURI), + Compression: util.StrToPtr(zzzCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr(randomURI), + Compression: util.StrToPtr(randomCompression), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + { + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr("invalid"), + }, + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "source"), + }, + { + From: path.New("yaml", "append", 0, "local"), + To: path.New("json", "append", 0, "compression"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "source"), + }, + { + From: path.New("yaml", "append", 1, "inline"), + To: path.New("json", "append", 1, "compression"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "source"), + }, + { + From: path.New("yaml", "append", 2, "local"), + To: path.New("json", "append", 2, "compression"), + }, + { + From: path.New("yaml", "append", 3, "inline"), + To: path.New("json", "append", 3, "source"), + }, + { + From: path.New("yaml", "append", 4, "local"), + To: path.New("json", "append", 4, "source"), + }, + }, + "", + common.TranslateOptions{ + FilesDir: filesDir, + }, + }, + // Test disable automatic gzip compression + { + File{ + Path: "/foo", + Contents: Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + types.File{ + Node: types.Node{ + Path: "/foo", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + []translate.Translation{ + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "source"), + }, + { + From: path.New("yaml", "contents", "inline"), + To: path.New("json", "contents", "compression"), + }, + }, + "", + common.TranslateOptions{ + NoResourceAutoCompression: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateFile(test.in, test.options) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateDirectory tests translating the ct storage.directories.[i] entries to ignition storage.directories.[i] entires. +func TestTranslateDirectory(t *testing.T) { + tests := []struct { + in Directory + out types.Directory + }{ + { + Directory{}, + types.Directory{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Directory{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Mode: util.IntToPtr(420), + Overwrite: util.BoolToPtr(true), + }, + types.Directory{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(420), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateDirectory(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLink tests translating the ct storage.links.[i] entries to ignition storage.links.[i] entires. +func TestTranslateLink(t *testing.T) { + tests := []struct { + in Link + out types.Link + }{ + { + Link{}, + types.Link{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Link{ + Path: "/foo", + Group: NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + types.Link{ + Node: types.Node{ + Path: "/foo", + Group: types.NodeGroup{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("foobar"), + }, + User: types.NodeUser{ + ID: util.IntToPtr(1), + Name: util.StrToPtr("bazquux"), + }, + Overwrite: util.BoolToPtr(true), + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("/bar"), + Hard: util.BoolToPtr(false), + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateLink(test.in, 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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateFilesystem tests translating the butane storage.filesystems.[i] entries to ignition storage.filesystems.[i] entries. +func TestTranslateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out types.Filesystem + }{ + { + Filesystem{}, + types.Filesystem{}, + }, + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []string{"yes", "no", "maybe"}, + Options: []string{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + WithMountUnit: util.BoolToPtr(true), + }, + types.Filesystem{ + Device: "/foo", + Format: util.StrToPtr("/bar"), + Label: util.StrToPtr("/baz"), + MountOptions: []types.MountOption{"yes", "no", "maybe"}, + Options: []types.FilesystemOption{"foo", "foo", "bar"}, + Path: util.StrToPtr("/quux"), + UUID: util.StrToPtr("1234"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + // Filesystem doesn't have a custom translator, so embed in a + // complete config + in := Config{ + Storage: Storage{ + Filesystems: []Filesystem{test.in}, + }, + } + expected := []types.Filesystem{test.out} + actual, translations, r := in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expected, actual.Storage.Filesystems, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + // FIXME: Zero values are pruned from merge transcripts and + // TranslationSets to make them more compact in debug output + // and tests. As a result, if the user specifies an empty + // struct in a list, the translation coverage will be + // incomplete and the report entry marker will end up + // pointing to the base of the list, or to a parent if the + // struct is the only entry in the list. Skip the coverage + // test for this case. + if !reflect.ValueOf(test.out).IsZero() { + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + } + }) + } +} + +// TestTranslateMountUnit tests the Butane storage.filesystems.[i].with_mount_unit flag. +func TestTranslateMountUnit(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // local mount with options, overridden enabled flag + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Enabled: util.BoolToPtr(false), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(false), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 +Options=ro,noatime + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []string{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + MountOptions: []types.MountOption{"ro", "noatime"}, + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=ro,noatime,_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // local mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-disk-by\x2dlabel-foo.service +After=systemd-fsck@dev-disk-by\x2dlabel-foo.service + +[Mount] +Where=/var/lib/containers +What=/dev/disk/by-label/foo +Type=ext4 + +[Install] +RequiredBy=local-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // remote mount, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + }, + }, + }, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Unit] +Requires=systemd-fsck@dev-mapper-foo\x2dbar.service +After=systemd-fsck@dev-mapper-foo\x2dbar.service + +[Mount] +Where=/var/lib/containers +What=/dev/mapper/foo-bar +Type=ext4 +Options=_netdev + +[Install] +RequiredBy=remote-fs.target`), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // overridden mount unit + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + Systemd: Systemd{ + Units: []Unit{ + { + Name: "var-lib-containers.mount", + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr("[Service]\nExecStart=/bin/false\n"), + Name: "var-lib-containers.mount", + }, + }, + }, + }, + }, + // swap, no options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + // swap with options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []string{"pri=1", "discard=pages"}, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/foo", + Format: util.StrToPtr("swap"), + MountOptions: []types.MountOption{"pri=1", "discard=pages"}, + }, + }, + }, + Systemd: types.Systemd{ + Units: []types.Unit{ + { + Enabled: util.BoolToPtr(true), + Contents: util.StrToPtr(`# Generated by Butane +[Swap] +What=/dev/disk/by-label/foo +Options=pri=1,discard=pages + +[Install] +RequiredBy=swap.target`), + Name: "dev-disk-by\\x2dlabel-foo.swap", + }, + }, + }, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + out, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, out, "bad output") + assert.Equal(t, report.Report{}, r, "expected empty report") + assert.NoError(t, translations.DebugVerifyCoverage(out), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateTree tests translating the butane storage.trees.[i] entries to ignition storage.files.[i] entries. +func TestTranslateTree(t *testing.T) { + deepPath := "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file" + deepPathURI, deepPathCompression := baseutil.CompressDataURL(t, []byte(deepPath)) + + tests := []struct { + options *common.TranslateOptions // defaulted if not specified + dirDirs map[string]os.FileMode // relative path -> mode + dirFiles map[string]os.FileMode // relative path -> mode + dirLinks map[string]string // relative path -> target + dirSockets []string // relative path + inTrees []Tree + inFiles []File + inDirs []Directory + inLinks []Link + outFiles []types.File + outDirs []types.Directory + outLinks []types.Link + report string + skip func(t *testing.T) + }{ + // smoke test + {}, + // basic functionality + { + dirFiles: map[string]os.FileMode{ + "tree/executable": 0700, + "tree/file": 0600, + "tree/overridden": 0644, + "tree/overridden-executable": 0700, + "tree/subdir/file": 0644, + // compressed contents + "tree/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/bad-link": "../nonexistent", + "tree/subdir/link": "../file", + "tree/subdir/overridden-link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + Path: util.StrToPtr("/etc"), + }, + }, + inFiles: []File{ + { + Path: "/overridden", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + { + Path: "/overridden-executable", + Mode: util.IntToPtr(0600), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + inLinks: []Link{ + { + Path: "/subdir/overridden-link", + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/overridden", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/overridden-executable", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Foverridden-executable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0600), + }, + }, + { + Node: types.Node{ + Path: "/executable", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fexecutable"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(func() int { + if runtime.GOOS != "windows" { + return 0755 + } else { + // Windows doesn't have executable bits + return 0644 + } + }()), + }, + }, + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(deepPathURI), + Compression: util.StrToPtr(deepPathCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/overridden-link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/bad-link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../nonexistent"), + }, + }, + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // TranslationSet completeness without overrides + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + }, + dirDirs: map[string]os.FileMode{ + "tree/dir": 0700, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // collisions + { + dirFiles: map[string]os.FileMode{ + "tree0/file": 0600, + "tree1/directory": 0600, + "tree2/link": 0600, + "tree3/file-partial": 0600, // should be okay + "tree4/link-partial": 0600, + "tree5/tree-file": 0600, // set up for tree/tree collision + "tree6/tree-file": 0600, + "tree15/tree-link": 0600, + }, + dirLinks: map[string]string{ + "tree7/file": "file", + "tree8/directory": "file", + "tree9/link": "file", + "tree10/file-partial": "file", + "tree11/link-partial": "file", // should be okay + "tree12/tree-file": "file", + "tree13/tree-link": "file", // set up for tree/tree collision + "tree14/tree-link": "file", + }, + inTrees: []Tree{ + { + Local: "tree0", + }, + { + Local: "tree1", + }, + { + Local: "tree2", + }, + { + Local: "tree3", + }, + { + Local: "tree4", + }, + { + Local: "tree5", + }, + { + Local: "tree6", + }, + { + Local: "tree7", + }, + { + Local: "tree8", + }, + { + Local: "tree9", + }, + { + Local: "tree10", + }, + { + Local: "tree11", + }, + { + Local: "tree12", + }, + { + Local: "tree13", + }, + { + Local: "tree14", + }, + { + Local: "tree15", + }, + }, + inFiles: []File{ + { + Path: "/file", + Contents: Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + { + Path: "/file-partial", + }, + }, + inDirs: []Directory{ + { + Path: "/directory", + }, + }, + inLinks: []Link{ + { + Path: "/link", + Target: util.StrToPtr("file"), + }, + { + Path: "/link-partial", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.1: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.2: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.4: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.6: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.7: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.8: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.9: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.10: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.12: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.14: " + common.ErrNodeExists.Error() + "\n" + + "error at $.storage.trees.15: " + common.ErrNodeExists.Error() + "\n", + }, + // files-dir escape + { + inTrees: []Tree{ + { + Local: "../escape", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFilesDirEscape.Error() + "\n", + }, + // no files-dir + { + options: &common.TranslateOptions{}, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNoFilesDir.Error() + "\n", + }, + // non-file/dir/symlink in directory tree + { + dirSockets: []string{ + "tree/socket", + }, + inTrees: []Tree{ + { + Local: "tree", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrFileType.Error() + "\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows supports Unix domain sockets, but os.Stat() + // doesn't detect them correctly. + t.Skip("skipping test due to https://github.com/golang/go/issues/33357") + } + }, + }, + // unreadable file + { + dirDirs: map[string]os.FileMode{ + "tree/subdir": 0000, + "tree2": 0000, + }, + dirFiles: map[string]os.FileMode{ + "tree/file": 0000, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "tree2", + }, + }, + report: "error at $.storage.trees.0: open %FilesDir%/tree/file: permission denied\n" + + "error at $.storage.trees.0: open %FilesDir%/tree/subdir: permission denied\n" + + "error at $.storage.trees.1: open %FilesDir%/tree2: permission denied\n", + skip: func(t *testing.T) { + if runtime.GOOS == "windows" { + // os.Chmod() only respects the writable bit and there + // isn't a trivial way to make inodes inaccessible + t.Skip("skipping test on Windows") + } + }, + }, + // local is not a directory + { + dirFiles: map[string]os.FileMode{ + "tree": 0600, + }, + inTrees: []Tree{ + { + Local: "tree", + }, + { + Local: "nonexistent", + }, + }, + report: "error at $.storage.trees.0: " + common.ErrTreeNotDirectory.Error() + "\n" + + "error at $.storage.trees.1: " + osStatName + " %FilesDir%" + string(filepath.Separator) + "nonexistent: " + osNotFound + "\n", + }, + // Permissions and ownership + { + dirFiles: map[string]os.FileMode{ + "tree/file": 0600, + "tree/subdir/file": 0644, + "tree2/file": 0600, + }, + dirLinks: map[string]string{ + "tree/subdir/link": "../file", + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + { + Local: "tree2", + DirMode: util.IntToPtr(0777), + Path: util.StrToPtr("/etc"), + }, + }, + outDirs: []types.Directory{ + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + Path: "/subdir", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0755), + }, + }, + { + Node: types.Node{ + Path: "/etc", + }, + DirectoryEmbedded1: types.DirectoryEmbedded1{ + Mode: util.IntToPtr(0777), + }, + }, + }, + outFiles: []types.File{ + { + Node: types.Node{ + Path: "/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/subdir/file", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree%2Fsubdir%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0777), + }, + }, + { + Node: types.Node{ + Path: "/etc/file", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,tree2%2Ffile"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + outLinks: []types.Link{ + { + Node: types.Node{ + Path: "/subdir/link", + User: types.NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: types.NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("../file"), + }, + }, + }, + }, + // Overwrite via tree ownership fails + { + dirFiles: map[string]os.FileMode{ + "tree/etc/file": 0600, + }, + inDirs: []Directory{ + {Path: "/etc"}, + }, + inTrees: []Tree{ + { + Local: "tree", + FileMode: util.IntToPtr(0777), + User: NodeUser{ + Name: util.StrToPtr("bovik"), + }, + Group: NodeGroup{ + ID: util.IntToPtr(1000), + }, + }, + }, + report: "error at $.storage.trees.0: " + common.ErrNodeExists.Error() + "\n", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + if test.skip != nil { + // give the test an opportunity to skip + test.skip(t) + } + filesDir := t.TempDir() + for testPath, mode := range test.dirDirs { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(absPath, 0755); err != nil { + t.Error(err) + return + } + if err := os.Chmod(absPath, mode); err != nil { + t.Error(err) + return + } + } + for testPath, mode := range test.dirFiles { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.WriteFile(absPath, []byte(testPath), mode); err != nil { + t.Error(err) + return + } + } + for testPath, target := range test.dirLinks { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + if err := os.Symlink(target, absPath); err != nil { + t.Error(err) + return + } + } + for _, testPath := range test.dirSockets { + absPath := filepath.Join(filesDir, filepath.FromSlash(testPath)) + if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { + t.Error(err) + return + } + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: absPath, + Net: "unix", + }) + if err != nil { + t.Error(err) + return + } + defer func() { _ = listener.Close() }() + } + + config := Config{ + Storage: Storage{ + Files: test.inFiles, + Directories: test.inDirs, + Links: test.inLinks, + Trees: test.inTrees, + }, + } + options := common.TranslateOptions{ + FilesDir: filesDir, + } + if test.options != nil { + options = *test.options + } + actual, translations, r := config.ToIgn3_7Unvalidated(options) + + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, config, r) + expectedReport := strings.ReplaceAll(test.report, "%FilesDir%", filesDir) + assert.Equal(t, expectedReport, r.String(), "bad report") + if expectedReport != "" { + return + } + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + + assert.Equal(t, test.outFiles, actual.Storage.Files, "files mismatch") + assert.Equal(t, test.outDirs, actual.Storage.Directories, "directories mismatch") + assert.Equal(t, test.outLinks, actual.Storage.Links, "links mismatch") + }) + } +} + +// TestTranslateIgnition tests translating the ct config.ignition to the ignition config.ignition section. +// It ensures that the version is set as well. +func TestTranslateIgnition(t *testing.T) { + tests := []struct { + in Ignition + out types.Ignition + }{ + { + Ignition{}, + types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + { + Ignition{ + Config: IgnitionConfig{ + Merge: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + Replace: Resource{ + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Config: types.IgnitionConfig{ + Merge: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + Replace: types.Resource{ + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + { + Ignition{ + Proxy: Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []string{"example.com"}, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Proxy: types.Proxy{ + HTTPProxy: util.StrToPtr("https://example.com:8080"), + NoProxy: []types.NoProxyItem{types.NoProxyItem("example.com")}, + }, + }, + }, + { + Ignition{ + Security: Security{ + TLS: TLS{ + CertificateAuthorities: []Resource{ + { + Inline: util.StrToPtr("xyzzy"), + }, + }, + }, + }, + }, + types.Ignition{ + Version: "3.7.0-experimental", + Security: types.Security{ + TLS: types.TLS{ + CertificateAuthorities: []types.Resource{ + { + Source: util.StrToPtr("data:,xyzzy"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := translateIgnition(test.in, 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") + // DebugVerifyCoverage wants to see a translation for $.version but + // translateIgnition doesn't create one; ToIgn3_*Unvalidated handles + // that since it has access to the Butane config version + translations.AddTranslation(path.New("yaml", "bogus"), path.New("json", "version")) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateKernelArguments tests translating the butane kernel_arguments.{should_exist,should_not_exist}.[i] entries to +// ignition kernelArguments.{shouldExist,shouldNotExist}.[i] entries. +// +// KernelArguments do not use a custom translation function (it utilizes the MergeP2 functionality) so pass an entire config +func TestTranslateKernelArguments(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{ + KernelArguments: KernelArguments{ + ShouldExist: []KernelArgument{ + "foo", + }, + ShouldNotExist: []KernelArgument{ + "bar", + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + KernelArguments: types.KernelArguments{ + ShouldExist: []types.KernelArgument{ + "foo", + }, + ShouldNotExist: []types.KernelArgument{ + "bar", + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateLuks test translating the butane storage.luks.clevis.tang.[i] arguments to ignition storage.luks.clevis.tang.[i] entries. +func TestTranslateTang(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + // Luks with tang and all options set, returns a valid ignition config with the same options + { + Config{ + Storage: Storage{ + Filesystems: []Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: Clevis{ + Tang: []Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/foo-bar", + Path: util.StrToPtr("/var/lib/containers"), + }, + }, + Luks: []types.Luks{ + { + Name: "foo-bar", + Device: util.StrToPtr("/dev/bar"), + Clevis: types.Clevis{ + Tang: []types.Tang{ + { + URL: "http://example.com", + Thumbprint: util.StrToPtr("xyzzy"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateSSHAuthorizedKey tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries. +func TestTranslateSSHAuthorizedKey(t *testing.T) { + sshKeyDir := t.TempDir() + randomDir := t.TempDir() + var sshKeyInline = "ssh-rsa AAAAAAAAA" + var sshKey1 = "ssh-rsa BBBBBBBBB" + var sshKey2 = "ssh-rsa CCCCCCCCC" + var sshKey3 = "ssh-rsa DDDDDDDDD" + var sshKeyFileName = "id_rsa.pub" + var sshKeyMultipleKeysFileName = "multiple.pub" + var sshKeyEmptyFileName = "empty.pub" + var sshKeyBlankFileName = "blank.pub" + var sshKeyWindowsLineEndingsFileName = "windows.pub" + var sshKeyNonExistingFileName = "id_ed25519.pub" + + sshKeyData := map[string][]byte{ + sshKeyFileName: []byte(sshKey1), + sshKeyMultipleKeysFileName: []byte(fmt.Sprintf("%s\n#comment\n\n\n%s\n", sshKey2, sshKey3)), + sshKeyEmptyFileName: []byte(""), + sshKeyBlankFileName: []byte("\n\t"), + sshKeyWindowsLineEndingsFileName: []byte(fmt.Sprintf("%s\r\n#comment\r\n", sshKey1)), + } + + for fileName, contents := range sshKeyData { + if err := os.WriteFile(filepath.Join(sshKeyDir, fileName), contents, 0644); err != nil { + t.Error(err) + } + } + + tests := []struct { + name string + in PasswdUser + out types.PasswdUser + translations []translate.Translation + report string + fileDir string + }{ + { + "empty user", + PasswdUser{}, + types.PasswdUser{}, + []translate.Translation{}, + "", + sshKeyDir, + }, + { + "valid inline keys", + PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + }, + "", + sshKeyDir, + }, + { + "valid multiple local key files", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName, sshKeyMultipleKeysFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 1), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid local and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKeyInline), types.SSHAuthorizedKey(sshKey1)}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "valid local keys with multiple keys per file and inline keys", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKeyInline)}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKeyInline), + types.SSHAuthorizedKey(sshKey2), + types.SSHAuthorizedKey("#comment"), + types.SSHAuthorizedKey(sshKey3), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 2)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 3)}, + }, + "", + sshKeyDir, + }, + { + "valid empty local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "", + sshKeyDir, + }, + { + "valid blank local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey("\t")}}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + }, + "", + sshKeyDir, + }, + { + "valid Windows style line endings in local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyWindowsLineEndingsFileName}}, + types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{ + types.SSHAuthorizedKey(sshKey1), + types.SSHAuthorizedKey("#comment"), + }}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 0)}, + {From: path.New("yaml", "ssh_authorized_keys_local", 0), To: path.New("json", "sshAuthorizedKeys", 1)}, + }, + "", + sshKeyDir, + }, + { + "missing local file", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(sshKeyDir, sshKeyNonExistingFileName) + ": " + osNotFound + "\n", + sshKeyDir, + }, + { + "missing embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}}, + types.PasswdUser{}, + []translate.Translation{ + {From: path.New("yaml", "ssh_authorized_keys_local"), To: path.New("json", "sshAuthorizedKeys")}, + }, + "error at $.ssh_authorized_keys_local.0: open " + filepath.Join(randomDir, sshKeyFileName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateUnitLocal tests translating the butane systemd.units[i].contents_local entries to ignition systemd.units[i].contents entries. +func TestTranslateUnitLocal(t *testing.T) { + unitDir := t.TempDir() + randomDir := t.TempDir() + var unitName = "example.service" + var dropinName = "example.conf" + var unitDefinitionInline = "[Service]\nExecStart=/bin/false\n" + var unitDefinitionFile = "[Service]\nExecStart=/bin/true\n" + var unitEmptyFileName = "empty.service" + var unitEmptyDefinition = "" + var unitNonExistingFileName = "random.service" + + err := os.WriteFile(filepath.Join(unitDir, unitName), []byte(unitDefinitionFile), 0644) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(unitDir, unitEmptyFileName), []byte(unitEmptyDefinition), 0644) + if err != nil { + t.Error(err) + } + + tests := []struct { + name string + in Unit + out types.Unit + translations []translate.Translation + report string + fileDir string + }{ + { + "empty unit", + Unit{}, + types.Unit{}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents", + Unit{Contents: &unitDefinitionInline, Name: unitName}, + types.Unit{Contents: &unitDefinitionInline, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid contents_local", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Contents: &unitDefinitionFile, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "non existing contents_local file name", + Unit{ContentsLocal: &unitNonExistingFileName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty contents_local file", + Unit{ContentsLocal: &unitEmptyFileName, Name: unitName}, + types.Unit{Contents: &unitEmptyDefinition, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "contents_local"), To: path.New("json", "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory", + Unit{ContentsLocal: &unitName, Name: unitName}, + types.Unit{Name: unitName}, + []translate.Translation{}, + "error at $.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + { + "empty dropin unit", + Unit{Name: dropinName, Dropins: nil}, + types.Unit{Name: dropinName, Dropins: nil}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents", + Unit{Dropins: []Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionInline}}, Name: unitName}, + []translate.Translation{}, + "", + "", + }, + { + "valid dropin contents_local", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitDefinitionFile}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "non existing dropin contents_local file name", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitNonExistingFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(unitDir, unitNonExistingFileName) + ": " + osNotFound + "\n", + unitDir, + }, + { + "valid empty dropin contents_local file", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitEmptyFileName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName, Contents: &unitEmptyDefinition}}, Name: unitName}, + []translate.Translation{ + {From: path.New("yaml", "dropins", 0, "contents_local"), To: path.New("json", "dropins", 0, "contents")}, + }, + "", + unitDir, + }, + { + "missing embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: " + common.ErrNoFilesDir.Error() + "\n", + "", + }, + { + "wrong embed directory for dropin", + Unit{Dropins: []Dropin{{Name: dropinName, ContentsLocal: &unitName}}, Name: unitName}, + types.Unit{Dropins: []types.Dropin{{Name: dropinName}}, Name: unitName}, + []translate.Translation{}, + "error at $.dropins.0.contents_local: open " + filepath.Join(randomDir, unitName) + ": " + osNotFound + "\n", + randomDir, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, translations, r := translateUnit(test.in, common.TranslateOptions{FilesDir: test.fileDir}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r.String(), "bad report") + baseutil.VerifyTranslations(t, translations, test.translations) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestToIgn3_7 tests the config.ToIgn3_7 function ensuring it will generate a valid config even when empty. Not much else is +// tested since it uses the Ignition translation code which has its own set of tests. +func TestToIgn3_7(t *testing.T) { + tests := []struct { + in Config + out types.Config + }{ + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(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") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestTranslateQuadlets(t *testing.T) { + const ( + SleepContainer = `[Unit] +Description=A sleepy container +[Container] +ContainerName=sleepy-pod-inf +Image=quay.io/fedora/fedora +Exec=sleep infinity +[Install] +WantedBy=multi-user.target` + + SleepContainerTemplate = `[Unit] +Description=A templated sleepy container +[Container] +Image=quay.io/fedora/fedora +Exec=sleep %i +[Service] +# Restart service when sleep finishes +Restart=always +[Install] +WantedBy=multi-user.target` + ) + + sleepContainerAsData, sleepContainerCompression := baseutil.CompressDataURL(t, []byte(SleepContainer)) + sleepContainerTemplateAsData, sleepContainerTemplateCompression := baseutil.CompressDataURL(t, []byte(SleepContainerTemplate)) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "sample.container": SleepContainer, + "sample@.container": SleepContainerTemplate, + "foo.conf": "[Service]\nTimeoutStartSec=900", + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + name string + inputConfig Config + outConf types.Config + reportPath string + options common.TranslateOptions + }{ + { + name: "Basic .container quadlets", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: false, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(sleepContainerAsData), + Compression: util.StrToPtr(sleepContainerCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/containers/systemd/users/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(sleepContainerAsData), + Compression: util.StrToPtr(sleepContainerCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + }, + }, + }, + { + name: "Template instance is symlink", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy@.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + }, + { + Name: "sleepy@100.container", + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(sleepContainerTemplateAsData), + Compression: util.StrToPtr(sleepContainerTemplateCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + Links: []types.Link{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("sleepy@.container"), + }, + }, + }, + }, + }, + }, + { + name: "Quadlet with non-existent contents_local", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + ContentsLocal: util.StrToPtr("fake-file.container"), + Rootful: true, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + reportPath: "error at $.systemd.quadlets.0.contents_local: open " + filepath.Join(filesDir, "fake-file.container") + ": " + osNotFound + "\n", + }, + { + name: "Quadlet with contents_local", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + ContentsLocal: util.StrToPtr("sample.container"), + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(sleepContainerAsData), + Compression: util.StrToPtr(sleepContainerCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + }, + { + name: "Overrides break", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + // two quadlets with the same name should give an error + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{}, + reportPath: "error at $.systemd.quadlets.1: matching filesystem node has existing contents or different type\n", + }, + { + name: "Template with dropin", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy@.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + Dropins: []Dropin{ + { + Name: "sample.conf", + Contents: util.StrToPtr("[Service]\nTimeoutStartSec=900"), + }, + }, + }, + { + Name: "sleepy@100.container", + Rootful: true, + Dropins: []Dropin{ + { + Name: "foo.conf", + ContentsLocal: util.StrToPtr("foo.conf"), + }, + }, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(sleepContainerTemplateAsData), + Compression: util.StrToPtr(sleepContainerTemplateCompression), + }, + Mode: util.IntToPtr(0644), + }, + }, + { // Dropin for all sleepy@.container instances + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container.d/sample.conf", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,%5BService%5D%0ATimeoutStartSec%3D900"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { // Dropin for the instance of sleepy@.container with 100 as input + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container.d/foo.conf", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,%5BService%5D%0ATimeoutStartSec%3D900"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + Links: []types.Link{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("sleepy@.container"), + }, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + }, + { + name: "Duplicate names will lead to file-collision", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + }, + }, + }, + reportPath: "error at $.systemd.quadlets.1: matching filesystem node has existing contents or different type\n", + }, + { + name: "File collision with trees", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sample.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + }, + }, + Storage: Storage{ + Trees: []Tree{ + { + Path: util.StrToPtr("/etc/containers/systemd"), + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + reportPath: "error at $.systemd.quadlets.0: matching filesystem node has existing contents or different type\n", + }, + { + name: "File collision", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + }, + }, + Storage: Storage{ + Files: []File{ + { + Path: "/etc/containers/systemd/sleepy.container", + }, + }, + }, + }, + reportPath: "error at $.systemd.quadlets.0: matching filesystem node has existing contents or different type\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c, _, r := test.inputConfig.ToIgn3_7Unvalidated(test.options) + assert.Equal(t, test.outConf, c, "translation mismatch") + assert.Equal(t, test.reportPath, r.String(), "report mismatch") + }) + } +} diff --git a/butane/base/v0_8_exp/util.go b/butane/base/v0_8_exp/util.go new file mode 100644 index 000000000..69a7d5099 --- /dev/null +++ b/butane/base/v0_8_exp/util.go @@ -0,0 +1,158 @@ +// 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 v0_8_exp + +import ( + common "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + vvalidate "github.com/coreos/vcontext/validate" +) + +type nodeTracker struct { + files *[]types.File + fileMap map[string]int + + dirs *[]types.Directory + dirMap map[string]int + + links *[]types.Link + linkMap map[string]int +} + +func newNodeTracker(c *types.Config) *nodeTracker { + t := nodeTracker{ + files: &c.Storage.Files, + fileMap: make(map[string]int, len(c.Storage.Files)), + + dirs: &c.Storage.Directories, + dirMap: make(map[string]int, len(c.Storage.Directories)), + + links: &c.Storage.Links, + linkMap: make(map[string]int, len(c.Storage.Links)), + } + for i, n := range *t.files { + t.fileMap[n.Path] = i + } + for i, n := range *t.dirs { + t.dirMap[n.Path] = i + } + for i, n := range *t.links { + t.linkMap[n.Path] = i + } + return &t +} + +func (t *nodeTracker) Exists(path string) bool { + for _, m := range []map[string]int{t.fileMap, t.dirMap, t.linkMap} { + if _, ok := m[path]; ok { + return true + } + } + return false +} + +func (t *nodeTracker) GetFile(path string) (int, *types.File) { + if i, ok := t.fileMap[path]; ok { + return i, &(*t.files)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddFile(f types.File) (int, *types.File) { + if f.Path == "" { + panic("File path missing") + } + if _, ok := t.fileMap[f.Path]; ok { + panic("Adding already existing file") + } + i := len(*t.files) + *t.files = append(*t.files, f) + t.fileMap[f.Path] = i + return i, &(*t.files)[i] +} + +func (t *nodeTracker) GetDir(path string) (int, *types.Directory) { + if i, ok := t.dirMap[path]; ok { + return i, &(*t.dirs)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddDir(d types.Directory) (int, *types.Directory) { + if d.Path == "" { + panic("Directory path missing") + } + if _, ok := t.dirMap[d.Path]; ok { + panic("Adding already existing directory") + } + i := len(*t.dirs) + *t.dirs = append(*t.dirs, d) + t.dirMap[d.Path] = i + return i, &(*t.dirs)[i] +} + +func (t *nodeTracker) GetLink(path string) (int, *types.Link) { + if i, ok := t.linkMap[path]; ok { + return i, &(*t.links)[i] + } else { + return 0, nil + } +} + +func (t *nodeTracker) AddLink(l types.Link) (int, *types.Link) { + if l.Path == "" { + panic("Link path missing") + } + if _, ok := t.linkMap[l.Path]; ok { + panic("Adding already existing link") + } + i := len(*t.links) + *t.links = append(*t.links, l) + t.linkMap[l.Path] = i + return i, &(*t.links)[i] +} + +func ValidateIgnitionConfig(c path.ContextPath, rawConfig []byte) (report.Report, error) { + r := report.Report{} + var config types.Config + rp, err := util.HandleParseErrors(rawConfig, &config) + if err != nil { + return rp, err + } + vrep := vvalidate.Validate(config.Ignition, "json") + skipValidate := false + if vrep.IsFatal() { + for _, e := range vrep.Entries { + // warn user with ErrUnknownVersion when version is unkown and skip the validation. + if e.Message == errors.ErrUnknownVersion.Error() { + skipValidate = true + r.AddOnWarn(c.Append("version"), common.ErrUnkownIgnitionVersion) + break + } + } + } + if !skipValidate { + report := validate.ValidateWithContext(config, rawConfig) + r.Merge(report) + } + return r, nil +} diff --git a/butane/base/v0_8_exp/validate.go b/butane/base/v0_8_exp/validate.go new file mode 100644 index 000000000..c2c60947c --- /dev/null +++ b/butane/base/v0_8_exp/validate.go @@ -0,0 +1,149 @@ +// 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 v0_8_exp + +import ( + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (rs Resource) Validate(c path.ContextPath) (r report.Report) { + var field string + sources := 0 + // Local files are validated in the translateResource function + if rs.Local != nil { + sources++ + field = "local" + } + if rs.Inline != nil { + sources++ + field = "inline" + } + if rs.Source != nil { + sources++ + field = "source" + } + if sources > 1 { + r.AddOnError(c.Append(field), common.ErrTooManyResourceSources) + return + } + if strings.HasPrefix(c.String(), "$.ignition.config") { + if field == "inline" { + rp, err := ValidateIgnitionConfig(c, []byte(*rs.Inline)) + r.Merge(rp) + if err != nil { + r.AddOnError(c.Append(field), err) + return + } + } + } + return +} + +func (fs Filesystem) Validate(c path.ContextPath) (r report.Report) { + if !util.IsTrue(fs.WithMountUnit) { + return + } + if util.NilOrEmpty(fs.Format) { + r.AddOnError(c.Append("format"), common.ErrMountUnitNoFormat) + } else if *fs.Format != "swap" && util.NilOrEmpty(fs.Path) { + r.AddOnError(c.Append("path"), common.ErrMountUnitNoPath) + } + return +} + +func (d Directory) Validate(c path.ContextPath) (r report.Report) { + if d.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*d.Mode, true)) + } + return +} + +func (f File) Validate(c path.ContextPath) (r report.Report) { + if f.Mode != nil { + r.AddOnWarn(c.Append("mode"), baseutil.CheckForDecimalMode(*f.Mode, false)) + } + return +} + +func (t Tree) Validate(c path.ContextPath) (r report.Report) { + if t.Local == "" { + r.AddOnError(c, common.ErrTreeNoLocal) + } + return +} + +func validateNotTooManySources(contentsLocal, contents *string, c path.ContextPath) (r report.Report) { + if contentsLocal != nil && contents != nil { + r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + } + return +} + +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + return validateNotTooManySources(rs.ContentsLocal, rs.Contents, c) +} + +func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { + return validateNotTooManySources(rs.ContentsLocal, rs.Contents, c) +} + +// All accepted extensions by podman-systemd.unit +func validateQuadletExtension(name string) error { + extensionIsSupported := strings.HasSuffix(name, ".container") || + strings.HasSuffix(name, ".volume") || + strings.HasSuffix(name, ".network") || + strings.HasSuffix(name, ".kube") || + strings.HasSuffix(name, ".image") || + strings.HasSuffix(name, ".build") || + strings.HasSuffix(name, ".pod") || + strings.HasSuffix(name, ".artifact") + + if !extensionIsSupported { + return common.ErrQuadletBadExtension + } + + return nil +} + +// Validate checks the quadlet name has a valid extension and template instances don't have contents. +func (rs Quadlet) Validate(c path.ContextPath) (r report.Report) { + if err := validateQuadletExtension(rs.Name); err != nil { + r.AddOnError(c.Append("name"), err) + return r + } + + // Template instances cannot have a content as they are symlinks, and non-template instances + // can have either a contents or a contents_local, but not both + if isTemplate, _ := isTemplateInstance(rs.Name); isTemplate { + if rs.Contents != nil { + contentPath := c.Append("contents") + r.AddOnError(contentPath, common.ErrTemplateInstanceCannotHaveContents) + } + if rs.ContentsLocal != nil { + contentPath := c.Append("contents_local") + r.AddOnError(contentPath, common.ErrTemplateInstanceCannotHaveContents) + } + } else { + r.Merge(validateNotTooManySources(rs.ContentsLocal, rs.Contents, c)) + } + return r +} diff --git a/butane/base/v0_8_exp/validate_test.go b/butane/base/v0_8_exp/validate_test.go new file mode 100644 index 000000000..6691ea2f2 --- /dev/null +++ b/butane/base/v0_8_exp/validate_test.go @@ -0,0 +1,513 @@ +// 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 v0_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestValidateResource tests that multiple sources (i.e. urls and inline) are not allowed but zero or one sources are +func TestValidateResource(t *testing.T) { + tests := []struct { + in Resource + out error + errPath path.ContextPath + }{ + {}, + // source specified + { + // contains invalid (by the validator's definition) combinations of fields, + // but the translator doesn't care and we can check they all get translated at once + Resource{ + Source: util.StrToPtr("http://example/com"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // inline specified + { + Resource{ + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // local specified + { + Resource{ + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + nil, + path.New("yaml"), + }, + // source + inline, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // source + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + // inline + local, invalid + { + Resource{ + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "inline"), + }, + // source + inline + local, invalid + { + Resource{ + Source: util.StrToPtr("data:,hello"), + Inline: util.StrToPtr("hello"), + Local: util.StrToPtr("hello"), + Compression: util.StrToPtr("gzip"), + Verification: Verification{ + Hash: util.StrToPtr("this isn't validated"), + }, + }, + common.ErrTooManyResourceSources, + path.New("yaml", "source"), + }, + } + + 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 TestValidateTree(t *testing.T) { + tests := []struct { + in Tree + out error + }{ + { + in: Tree{}, + out: common.ErrTreeNoLocal, + }, + } + + 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(path.New("yaml"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFileMode(t *testing.T) { + fileTests := []struct { + in File + out error + }{ + { + in: File{}, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(0600), + }, + out: nil, + }, + { + in: File{ + Mode: util.IntToPtr(600), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range fileTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateDirMode(t *testing.T) { + dirTests := []struct { + in Directory + out error + }{ + { + in: Directory{}, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(01770), + }, + out: nil, + }, + { + in: Directory{ + Mode: util.IntToPtr(1770), + }, + out: common.ErrDecimalMode, + }, + } + + for i, test := range dirTests { + 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.AddOnWarn(path.New("yaml", "mode"), test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateFilesystem(t *testing.T) { + tests := []struct { + in Filesystem + out error + errPath path.ContextPath + }{ + { + Filesystem{}, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + Path: util.StrToPtr("/z"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("swap"), + WithMountUnit: util.BoolToPtr(true), + }, + nil, + path.New("yaml"), + }, + { + Filesystem{ + Device: "/dev/foo", + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoFormat, + path.New("yaml", "format"), + }, + { + Filesystem{ + Device: "/dev/foo", + Format: util.StrToPtr("zzz"), + WithMountUnit: util.BoolToPtr(true), + }, + common.ErrMountUnitNoPath, + path.New("yaml", "path"), + }, + } + + 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") + }) + } +} + +// TestValidateUnit tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateUnit(t *testing.T) { + tests := []struct { + in Unit + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Unit{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Unit{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Unit{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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") + }) + } +} + +// TestValidateDropin tests that multiple sources (i.e. contents and contents_local) are not allowed but zero or one sources are +func TestValidateDropin(t *testing.T) { + tests := []struct { + in Dropin + out error + errPath path.ContextPath + }{ + {}, + // contents specified + { + Dropin{ + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents_local specified + { + Dropin{ + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // contents + contents_local, invalid + { + Dropin{ + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello, too"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + } + + 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 TestValidateQuadlet(t *testing.T) { + tests := []struct { + in Quadlet + reportPath string + }{ + { + Quadlet{ + Name: "working.container", + ContentsLocal: util.StrToPtr("hello"), + }, + "", + }, + { + Quadlet{ + Name: "working.container", + Contents: util.StrToPtr("hello"), + }, + "", + }, + { + Quadlet{ + Name: "bad-extension.foo", + Contents: util.StrToPtr("hello"), + }, + "error at $.name: " + common.ErrQuadletBadExtension.Error() + "\n", + }, + { + Quadlet{ + Name: "testing.container", + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello"), + }, + "error at $.contents_local: " + common.ErrTooManySystemdSources.Error() + "\n", + }, + // No contents and no contents_local is allowed + { + Quadlet{ + Name: "testing.container", + }, + "", + }, + { + Quadlet{ + Name: "templateBase@.container", + Contents: util.StrToPtr("hello"), + }, + "", + }, + // template instance cannot have contents + { + Quadlet{ + Name: "templateInstance@1000.container", + }, + "", + }, + { + Quadlet{ + Name: "templateInstance@1000.container", + ContentsLocal: util.StrToPtr("hello"), + }, + "error at $.contents_local: " + common.ErrTemplateInstanceCannotHaveContents.Error() + "\n", + }, + { + Quadlet{ + Name: "templateInstance@1000.container", + Contents: util.StrToPtr("hello"), + }, + "error at $.contents: " + common.ErrTemplateInstanceCannotHaveContents.Error() + "\n", + }, + { + Quadlet{ + Name: "", + }, + "error at $.name: " + common.ErrQuadletBadExtension.Error() + "\n", + }, + { + Quadlet{ + Name: "foo@bar", + }, + "error at $.name: " + common.ErrQuadletBadExtension.Error() + "\n", + }, + { + Quadlet{ + Name: "template@100.container", + Contents: util.StrToPtr("non-empty"), + ContentsLocal: util.StrToPtr("non-empty"), + }, + "error at $.contents: " + common.ErrTemplateInstanceCannotHaveContents.Error() + "\n" + + "error at $.contents_local: " + common.ErrTemplateInstanceCannotHaveContents.Error() + "\n", + }, + } + + 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) + assert.Equal(t, test.reportPath, actual.String(), "bad report") + }) + } +} + +// TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified +func TestUnkownIgnitionVersion(t *testing.T) { + test := struct { + in Resource + out error + errPath path.ContextPath + }{ + Resource{ + Inline: util.StrToPtr(`{"ignition": {"version": "10.0.0"}}`), + }, + common.ErrUnkownIgnitionVersion, + path.New("yaml", "ignition", "config", "version"), + } + path := path.New("yaml", "ignition", "config") + // Skipping baseutil.VerifyReport because it expects all referenced paths to exist in the struct. + // In this test, "ignition.config" doesn't exist, so VerifyReport would fail. However, we still need + // to pass this path to Validate() to trigger the unknown Ignition version warning we're testing for. + actual := test.in.Validate(path) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") +} diff --git a/butane/build b/butane/build new file mode 100755 index 000000000..1d2898aca --- /dev/null +++ b/butane/build @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -eu + +export GO111MODULE=on +export GOFLAGS=-mod=vendor +export CGO_ENABLED=0 + +if [ -z ${VERSION+a} ]; then + VERSION=$(git describe --dirty --always) +fi + +LDFLAGS="-w -X github.com/coreos/ignition/v2/butane/internal/version.Raw=$VERSION" + +NAME=butane + +if [ -z ${BIN_PATH+a} ]; then + BIN_PATH=${PWD}/bin/$(go env GOARCH) +fi + +echo "Building $NAME..." +go build -o ${BIN_PATH}/${NAME} -ldflags "$LDFLAGS" github.com/coreos/ignition/v2/butane/internal diff --git a/butane/build_for_container b/butane/build_for_container new file mode 100755 index 000000000..2bc05d884 --- /dev/null +++ b/butane/build_for_container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +export GO111MODULE=on +export GOFLAGS=-mod=vendor +export CGO_ENABLED=0 +version=$(git describe --dirty --always) +LDFLAGS="-w -X github.com/coreos/butane/internal/version.Raw=$version" + +eval $(go env) +if [ -z ${BIN_PATH+a} ]; then + export BIN_PATH=${PWD}/bin/container/ +fi + +export GOOS=linux +go build -o ${BIN_PATH}/butane -ldflags "$LDFLAGS" internal/main.go diff --git a/butane/config/common/common.go b/butane/config/common/common.go new file mode 100644 index 000000000..f760bfaa7 --- /dev/null +++ b/butane/config/common/common.go @@ -0,0 +1,27 @@ +// Copyright 2019 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 common + +type TranslateOptions struct { + FilesDir string // allow embedding local files relative to this directory + NoResourceAutoCompression bool // skip automatic compression of inline/local resources + DebugPrintTranslations bool // report translations to stderr +} + +type TranslateBytesOptions struct { + TranslateOptions + Pretty bool + Raw bool // encode only the Ignition config, not any wrapper +} diff --git a/butane/config/common/errors.go b/butane/config/common/errors.go new file mode 100644 index 000000000..70737750f --- /dev/null +++ b/butane/config/common/errors.go @@ -0,0 +1,131 @@ +// Copyright 2019 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 common + +import ( + "errors" + "fmt" + + "github.com/coreos/go-semver/semver" +) + +var ( + // common field parsing + ErrNoVariant = errors.New("error parsing variant; must be specified") + ErrInvalidVersion = errors.New("error parsing version; must be a valid semver") + + // high-level errors for fatal reports + ErrInvalidSourceConfig = errors.New("source config is invalid") + ErrInvalidGeneratedConfig = errors.New("config generated was invalid") + + // deprecated variant/version + ErrRhcosVariantUnsupported = errors.New("rhcos variant has been removed; use openshift variant instead: https://coreos.github.io/butane/upgrading-openshift/") + + // resources and trees + ErrTooManyResourceSources = errors.New("only one of the following can be set: inline, local, source") + ErrFilesDirEscape = errors.New("local file path traverses outside the files directory") + ErrFileType = errors.New("trees may only contain files, directories, and symlinks") + ErrNodeExists = errors.New("matching filesystem node has existing contents or different type") + ErrNoFilesDir = errors.New("local file paths are relative to a files directory that must be specified with -d/--files-dir") + ErrTreeNotDirectory = errors.New("root of tree must be a directory") + ErrTreeNoLocal = errors.New("local is required") + + // filesystem nodes + ErrDecimalMode = errors.New("unreasonable mode would be reasonable if specified in octal; remember to add a leading zero") + + // systemd + ErrTooManySystemdSources = errors.New("only one of the following can be set: contents, contents_local") + ErrQuadletBadExtension = errors.New("unsupported file extension for quadlet: must be one of .container, .volume, .network, .kube, .image, .build, .pod, or .artifact") + ErrTemplateInstanceCannotHaveContents = errors.New("template instances cannot have contents or contents_local") + + // mount units + ErrMountUnitNoPath = errors.New("path is required if with_mount_unit is true and format is not swap") + ErrMountUnitNoFormat = errors.New("format is required if with_mount_unit is true") + ErrMountPointForbidden = errors.New("path must be under /etc or /var if with_mount_unit is true") + + // boot device + ErrUnknownBootDeviceLayout = errors.New("layout must be one of: aarch64, ppc64le, s390x-eckd, s390x-virt, s390x-zfcp, x86_64") + ErrUnknownBootDeviceLayoutLegacy = errors.New("layout must be one of: aarch64, ppc64le, x86_64") + ErrTooFewMirrorDevices = errors.New("mirroring requires at least two devices") + ErrMirrorRequiresLayout = errors.New("boot_device.layout should be specified when boot_device.mirror is specified") + ErrNoLuksBootDevice = errors.New("device is required for layouts: s390x-eckd, s390x-zfcp") + ErrMirrorNotSupport = errors.New("mirroring not supported on layouts: s390x-eckd, s390x-zfcp, s390x-virt") + ErrLuksBootDeviceBadName = errors.New("device name must start with /dev/dasd on s390x-eckd layout or /dev/sd on s390x-zfcp layout") + ErrCexArchitectureMismatch = errors.New("when using cex the targeted architecture must match s390x") + ErrCexNotSupported = errors.New("cex is not currently supported on the target platform") + ErrNoLuksMethodSpecified = errors.New("no method specified for luks") + + // partition + ErrReuseByLabel = errors.New("partitions cannot be reused by label; number must be specified except on boot disk (/dev/disk/by-id/coreos-boot-disk) or when wipe_table is true") + ErrWrongPartitionNumber = errors.New("incorrect partition number; a new partition will be created using reserved label") + ErrRootTooSmall = errors.New("root should have 8GiB of space assigned") + ErrRootConstrained = errors.New("root partition cannot expand; it is set to fill available space but is followed by an auto-positioned partition") + + // MachineConfigs + ErrFieldElided = errors.New("field ignored in raw mode") + ErrNameRequired = errors.New("metadata.name is required") + ErrRoleRequired = errors.New("machineconfiguration.openshift.io/role label is required") + ErrInvalidKernelType = errors.New("must be empty, \"default\", or \"realtime\"") + ErrBtrfsSupport = errors.New("btrfs is not supported in this spec version") + ErrFilesystemNoneSupport = errors.New("format \"none\" is not supported in this spec version") + ErrFileSchemeSupport = errors.New("file contents source must be data URL in this spec version") + ErrFileAppendSupport = errors.New("appending to files is not supported in this spec version") + ErrFileCompressionSupport = errors.New("file compression is not supported in this spec version") + ErrFileHeaderSupport = errors.New("file HTTP headers are not supported in this spec version") + ErrFileSpecialModeSupport = errors.New("special mode bits are not supported in this spec version") + ErrGroupSupport = errors.New("groups are not supported in this spec version") + ErrUserFieldSupport = errors.New("fields other than \"name\", \"ssh_authorized_keys\", and \"password_hash\" (4.13.0+) are not supported in this spec version") + ErrUserNameSupport = errors.New("users other than \"core\" are not supported in this spec version") + ErrKernelArgumentSupport = errors.New("this section cannot be used for kernel arguments in this spec version; use openshift.kernel_arguments instead") + ErrMissingKernelArgumentCex = errors.New("'rd.luks.key=/etc/luks/cex.key' must be set as kernel argument when CEX is enabled for the boot device") + + // Storage + ErrClevisSupport = errors.New("clevis is not supported in this spec version") + ErrDirectorySupport = errors.New("directories are not supported in this spec version") + ErrDiskSupport = errors.New("disk customization is not supported in this spec version") + ErrFilesystemSupport = errors.New("filesystem customization is not supported in this spec version") + ErrLinkSupport = errors.New("links are not supported in this spec version") + ErrLuksSupport = errors.New("luks is not supported in this spec version") + ErrRaidSupport = errors.New("raid is not supported in this spec version") + + // Grub + ErrGrubUserNameNotSpecified = errors.New("field \"name\" is required") + ErrGrubPasswordNotSpecified = errors.New("field \"password_hash\" is required") + + // Kernel arguments + ErrGeneralKernelArgumentSupport = errors.New("kernel argument customization is not supported in this spec version") + + // Unkown ignition version + ErrUnkownIgnitionVersion = errors.New("skipping validation for the merge/replace ignition config due to an unkown version") +) + +type ErrUnmarshal struct { + // don't wrap the underlying error object because we don't want to + // commit to its API + Detail string +} + +func (e ErrUnmarshal) Error() string { + return fmt.Sprintf("Error unmarshaling yaml: %v", e.Detail) +} + +type ErrUnknownVersion struct { + Variant string + Version semver.Version +} + +func (e ErrUnknownVersion) Error() string { + return fmt.Sprintf("No translator exists for variant %s with version %s", e.Variant, e.Version) +} diff --git a/butane/config/config.go b/butane/config/config.go new file mode 100644 index 000000000..68314b1cc --- /dev/null +++ b/butane/config/config.go @@ -0,0 +1,165 @@ +// Copyright 2019 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 config + +import ( + "fmt" + + "github.com/coreos/ignition/v2/butane/config/common" + fcos1_0 "github.com/coreos/ignition/v2/butane/config/fcos/v1_0" + fcos1_1 "github.com/coreos/ignition/v2/butane/config/fcos/v1_1" + fcos1_2 "github.com/coreos/ignition/v2/butane/config/fcos/v1_2" + fcos1_3 "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + fcos1_4 "github.com/coreos/ignition/v2/butane/config/fcos/v1_4" + fcos1_5 "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + fcos1_6 "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + fcos1_7 "github.com/coreos/ignition/v2/butane/config/fcos/v1_7" + fcos1_8_exp "github.com/coreos/ignition/v2/butane/config/fcos/v1_8_exp" + fiot1_0 "github.com/coreos/ignition/v2/butane/config/fiot/v1_0" + fiot1_1_exp "github.com/coreos/ignition/v2/butane/config/fiot/v1_1_exp" + flatcar1_0 "github.com/coreos/ignition/v2/butane/config/flatcar/v1_0" + flatcar1_1 "github.com/coreos/ignition/v2/butane/config/flatcar/v1_1" + flatcar1_2_exp "github.com/coreos/ignition/v2/butane/config/flatcar/v1_2_exp" + openshift4_10 "github.com/coreos/ignition/v2/butane/config/openshift/v4_10" + openshift4_11 "github.com/coreos/ignition/v2/butane/config/openshift/v4_11" + openshift4_12 "github.com/coreos/ignition/v2/butane/config/openshift/v4_12" + openshift4_13 "github.com/coreos/ignition/v2/butane/config/openshift/v4_13" + openshift4_14 "github.com/coreos/ignition/v2/butane/config/openshift/v4_14" + openshift4_15 "github.com/coreos/ignition/v2/butane/config/openshift/v4_15" + openshift4_16 "github.com/coreos/ignition/v2/butane/config/openshift/v4_16" + openshift4_17 "github.com/coreos/ignition/v2/butane/config/openshift/v4_17" + openshift4_18 "github.com/coreos/ignition/v2/butane/config/openshift/v4_18" + openshift4_19 "github.com/coreos/ignition/v2/butane/config/openshift/v4_19" + openshift4_20 "github.com/coreos/ignition/v2/butane/config/openshift/v4_20" + openshift4_21 "github.com/coreos/ignition/v2/butane/config/openshift/v4_21" + openshift4_22 "github.com/coreos/ignition/v2/butane/config/openshift/v4_22" + openshift4_23_exp "github.com/coreos/ignition/v2/butane/config/openshift/v4_23_exp" + openshift4_8 "github.com/coreos/ignition/v2/butane/config/openshift/v4_8" + openshift4_9 "github.com/coreos/ignition/v2/butane/config/openshift/v4_9" + r4e1_0 "github.com/coreos/ignition/v2/butane/config/r4e/v1_0" + r4e1_1 "github.com/coreos/ignition/v2/butane/config/r4e/v1_1" + r4e1_2_exp "github.com/coreos/ignition/v2/butane/config/r4e/v1_2_exp" + + "github.com/coreos/go-semver/semver" + "github.com/coreos/vcontext/report" + "gopkg.in/yaml.v3" +) + +var ( + registry = map[string]translator{} +) + +// Fields that must be included in the root struct of every spec version. +type commonFields struct { + Version string `yaml:"version"` + Variant string `yaml:"variant"` +} + +func init() { + RegisterTranslator("fcos", "1.0.0", fcos1_0.ToIgn3_0Bytes) + RegisterTranslator("fcos", "1.1.0", fcos1_1.ToIgn3_1Bytes) + RegisterTranslator("fcos", "1.2.0", fcos1_2.ToIgn3_2Bytes) + RegisterTranslator("fcos", "1.3.0", fcos1_3.ToIgn3_2Bytes) + RegisterTranslator("fcos", "1.4.0", fcos1_4.ToIgn3_3Bytes) + RegisterTranslator("fcos", "1.5.0", fcos1_5.ToIgn3_4Bytes) + RegisterTranslator("fcos", "1.6.0", fcos1_6.ToIgn3_5Bytes) + RegisterTranslator("fcos", "1.7.0", fcos1_7.ToIgn3_6Bytes) + RegisterTranslator("fcos", "1.8.0-experimental", fcos1_8_exp.ToIgn3_7Bytes) + RegisterTranslator("flatcar", "1.0.0", flatcar1_0.ToIgn3_3Bytes) + RegisterTranslator("flatcar", "1.1.0", flatcar1_1.ToIgn3_4Bytes) + RegisterTranslator("flatcar", "1.2.0-experimental", flatcar1_2_exp.ToIgn3_7Bytes) + RegisterTranslator("openshift", "4.8.0", openshift4_8.ToConfigBytes) + RegisterTranslator("openshift", "4.9.0", openshift4_9.ToConfigBytes) + RegisterTranslator("openshift", "4.10.0", openshift4_10.ToConfigBytes) + RegisterTranslator("openshift", "4.11.0", openshift4_11.ToConfigBytes) + RegisterTranslator("openshift", "4.12.0", openshift4_12.ToConfigBytes) + RegisterTranslator("openshift", "4.13.0", openshift4_13.ToConfigBytes) + 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.ToConfigBytes) + RegisterTranslator("openshift", "4.18.0", openshift4_18.ToConfigBytes) + 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", 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) + RegisterTranslator("fiot", "1.0.0", fiot1_0.ToIgn3_4Bytes) + RegisterTranslator("fiot", "1.1.0-experimental", fiot1_1_exp.ToIgn3_7Bytes) + RegisterTranslator("rhcos", "0.1.0", unsupportedRhcosVariant) +} + +// RegisterTranslator registers a translator for the specified variant and +// version to be available for use by TranslateBytes. This is only needed +// by users implementing their own translators outside the Butane package. +func RegisterTranslator(variant, version string, trans translator) { + key := fmt.Sprintf("%s+%s", variant, version) + if _, ok := registry[key]; ok { + panic("tried to reregister existing translator") + } + registry[key] = trans +} + +func getTranslator(variant string, version semver.Version) (translator, error) { + t, ok := registry[fmt.Sprintf("%s+%s", variant, version.String())] + if !ok { + return nil, common.ErrUnknownVersion{ + Variant: variant, + Version: version, + } + } + return t, nil +} + +// translators take a raw config and translate it to a raw Ignition config. The report returned should include any +// errors, warnings, etc. and may or may not be fatal. If report is fatal, or other errors are encountered while translating +// translators should return an error. +type translator func([]byte, common.TranslateBytesOptions) ([]byte, report.Report, error) + +// TranslateBytes wraps all of the individual TranslateBytes functions in a switch that determines the correct one to call. +// TranslateBytes returns an error if the report had fatal errors or if other errors occured during translation. +func TranslateBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + // first determine version; this will ignore most fields + ver := commonFields{} + if err := yaml.Unmarshal(input, &ver); err != nil { + return nil, report.Report{}, common.ErrUnmarshal{ + Detail: err.Error(), + } + } + + if ver.Variant == "" { + return nil, report.Report{}, common.ErrNoVariant + } + + tmp, err := semver.NewVersion(ver.Version) + if err != nil { + return nil, report.Report{}, common.ErrInvalidVersion + } + version := *tmp + + translator, err := getTranslator(ver.Variant, version) + if err != nil { + return nil, report.Report{}, err + } + + return translator(input, options) +} + +func unsupportedRhcosVariant(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return nil, report.Report{}, common.ErrRhcosVariantUnsupported +} diff --git a/butane/config/fcos/v1_0/schema.go b/butane/config/fcos/v1_0/schema.go new file mode 100644 index 000000000..0dbd7e8b0 --- /dev/null +++ b/butane/config/fcos/v1_0/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_1" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/fcos/v1_0/translate.go b/butane/config/fcos/v1_0/translate.go new file mode 100644 index 000000000..d5f5b5c8c --- /dev/null +++ b/butane/config/fcos/v1_0/translate.go @@ -0,0 +1,73 @@ +// Copyright 2019 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 v1_0 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_0/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_0Unvalidated 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_0Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_0Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + + for i, disk := range ret.Storage.Disks { + // Don't warn if wipeTable is set, matching later spec versions + if !util.IsTrue(disk.WipeTable) { + for j, partition := range disk.Partitions { + // check for reserved partlabels + if partition.Label != nil { + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", j, "label"), common.ErrWrongPartitionNumber) + } + } + } + } + } + return ret, ts, r +} + +// ToIgn3_0 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_0(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_0Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_0Bytes translates from a v1.0 Butane config to a v3.0.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 ToIgn3_0Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_0", options) +} diff --git a/butane/config/fcos/v1_0/translate_test.go b/butane/config/fcos/v1_0/translate_test.go new file mode 100644 index 000000000..f0c5350c5 --- /dev/null +++ b/butane/config/fcos/v1_0/translate_test.go @@ -0,0 +1,157 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_1" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_0/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestTranslateConfig tests translating the Butane config. +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.0.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_0Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_0/validate_test.go b/butane/config/fcos/v1_0/validate_test.go new file mode 100644 index 000000000..0fdcbced1 --- /dev/null +++ b/butane/config/fcos/v1_0/validate_test.go @@ -0,0 +1,101 @@ +// 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 v1_0 + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/stretchr/testify/assert" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 6, // variant behavior in base 0.1 + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_0Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} diff --git a/butane/config/fcos/v1_1/schema.go b/butane/config/fcos/v1_1/schema.go new file mode 100644 index 000000000..09bb97f45 --- /dev/null +++ b/butane/config/fcos/v1_1/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_2" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/fcos/v1_1/translate.go b/butane/config/fcos/v1_1/translate.go new file mode 100644 index 000000000..dd3576de9 --- /dev/null +++ b/butane/config/fcos/v1_1/translate.go @@ -0,0 +1,73 @@ +// Copyright 2019 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 v1_1 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_1/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_1Unvalidated 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_1Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_1Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + + for i, disk := range ret.Storage.Disks { + // Don't warn if wipeTable is set, matching later spec versions + if !util.IsTrue(disk.WipeTable) { + for j, partition := range disk.Partitions { + // check for reserved partlabels + if partition.Label != nil { + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", j, "label"), common.ErrWrongPartitionNumber) + } + } + } + } + } + return ret, ts, r +} + +// ToIgn3_1 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_1(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_1Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_1Bytes translates from a v1.1 Butane config to a v3.1.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 ToIgn3_1Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_1", options) +} diff --git a/butane/config/fcos/v1_1/translate_test.go b/butane/config/fcos/v1_1/translate_test.go new file mode 100644 index 000000000..b79f9b26d --- /dev/null +++ b/butane/config/fcos/v1_1/translate_test.go @@ -0,0 +1,157 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_2" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_1/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestTranslateConfig tests translating the Butane config. +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.1.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_1Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_1/validate_test.go b/butane/config/fcos/v1_1/validate_test.go new file mode 100644 index 000000000..5cbcf4187 --- /dev/null +++ b/butane/config/fcos/v1_1/validate_test.go @@ -0,0 +1,111 @@ +// 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 v1_1 + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/stretchr/testify/assert" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_1Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} diff --git a/butane/config/fcos/v1_2/schema.go b/butane/config/fcos/v1_2/schema.go new file mode 100644 index 000000000..6bc3ad933 --- /dev/null +++ b/butane/config/fcos/v1_2/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_3" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/fcos/v1_2/translate.go b/butane/config/fcos/v1_2/translate.go new file mode 100644 index 000000000..01105ee23 --- /dev/null +++ b/butane/config/fcos/v1_2/translate.go @@ -0,0 +1,73 @@ +// Copyright 2019 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 v1_2 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_2Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + + for i, disk := range ret.Storage.Disks { + // Don't warn if wipeTable is set, matching later spec versions + if !util.IsTrue(disk.WipeTable) { + for j, partition := range disk.Partitions { + // check for reserved partlabels + if partition.Label != nil { + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", j, "label"), common.ErrWrongPartitionNumber) + } + } + } + } + } + return ret, ts, r +} + +// ToIgn3_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_2Bytes translates from a v1.2 Butane config to a v3.2.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 ToIgn3_2Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_2", options) +} diff --git a/butane/config/fcos/v1_2/translate_test.go b/butane/config/fcos/v1_2/translate_test.go new file mode 100644 index 000000000..3e44351e3 --- /dev/null +++ b/butane/config/fcos/v1_2/translate_test.go @@ -0,0 +1,160 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestTranslateConfig tests translating the Butane config. +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_2Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_2/validate_test.go b/butane/config/fcos/v1_2/validate_test.go new file mode 100644 index 000000000..9ddcf3a9a --- /dev/null +++ b/butane/config/fcos/v1_2/validate_test.go @@ -0,0 +1,111 @@ +// 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 v1_2 + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/stretchr/testify/assert" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_2Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} diff --git a/butane/config/fcos/v1_3/schema.go b/butane/config/fcos/v1_3/schema.go new file mode 100644 index 000000000..55ead41d0 --- /dev/null +++ b/butane/config/fcos/v1_3/schema.go @@ -0,0 +1,40 @@ +// 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 v1_3 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_3" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} diff --git a/butane/config/fcos/v1_3/translate.go b/butane/config/fcos/v1_3/translate.go new file mode 100644 index 000000000..dce129640 --- /dev/null +++ b/butane/config/fcos/v1_3/translate.go @@ -0,0 +1,317 @@ +// 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 v1_3 + +import ( + "fmt" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_2Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + + } + } + } + return ret, ts, r +} + +// ToIgn3_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_2Bytes translates from a v1.3 Butane config to a v3.2.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 ToIgn3_2Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_2", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: "raid1", + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: "raid1", + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + luksDevice := "/dev/disk/by-partlabel/root" + if wantMirror { + luksDevice = "/dev/md/md-root" + } + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: &clevis, + Device: &luksDevice, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} diff --git a/butane/config/fcos/v1_3/translate_test.go b/butane/config/fcos/v1_3/translate_test.go new file mode 100644 index 000000000..f83842b92 --- /dev/null +++ b/butane/config/fcos/v1_3/translate_test.go @@ -0,0 +1,1664 @@ +// 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 v1_3 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: &types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: "raid1", + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: "raid1", + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: "raid1", + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: "raid1", + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: &types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: "raid1", + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: "raid1", + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: &types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: "raid1", + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: "raid1", + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: &types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.2.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: "raid1", + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: "raid1", + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: &types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_2Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_2Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} diff --git a/butane/config/fcos/v1_3/validate.go b/butane/config/fcos/v1_3/validate.go new file mode 100644 index 000000000..a0a867434 --- /dev/null +++ b/butane/config/fcos/v1_3/validate.go @@ -0,0 +1,45 @@ +// 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 v1_3 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnWarn(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayoutLegacy) + } + } + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} diff --git a/butane/config/fcos/v1_3/validate_test.go b/butane/config/fcos/v1_3/validate_test.go new file mode 100644 index 000000000..122be96fd --- /dev/null +++ b/butane/config/fcos/v1_3/validate_test.go @@ -0,0 +1,180 @@ +// 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 v1_3 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_2Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayoutLegacy, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + } + + 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 validation report") + }) + } +} diff --git a/butane/config/fcos/v1_4/schema.go b/butane/config/fcos/v1_4/schema.go new file mode 100644 index 000000000..8654dc7ed --- /dev/null +++ b/butane/config/fcos/v1_4/schema.go @@ -0,0 +1,40 @@ +// 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 v1_4 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_4" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} diff --git a/butane/config/fcos/v1_4/translate.go b/butane/config/fcos/v1_4/translate.go new file mode 100644 index 000000000..10f6885ef --- /dev/null +++ b/butane/config/fcos/v1_4/translate.go @@ -0,0 +1,317 @@ +// 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 v1_4 + +import ( + "fmt" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_3Unvalidated 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_3Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_3Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + + } + } + } + return ret, ts, r +} + +// ToIgn3_3 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_3(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_3Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_3Bytes translates from a v1.4 Butane config to a v3.3.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 ToIgn3_3Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_3", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + luksDevice := "/dev/disk/by-partlabel/root" + if wantMirror { + luksDevice = "/dev/md/md-root" + } + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} diff --git a/butane/config/fcos/v1_4/translate_test.go b/butane/config/fcos/v1_4/translate_test.go new file mode 100644 index 000000000..5dcf332a1 --- /dev/null +++ b/butane/config/fcos/v1_4/translate_test.go @@ -0,0 +1,1664 @@ +// 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 v1_4 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_4" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.3.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_3Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_3Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} diff --git a/butane/config/fcos/v1_4/validate.go b/butane/config/fcos/v1_4/validate.go new file mode 100644 index 000000000..5b66e8d2b --- /dev/null +++ b/butane/config/fcos/v1_4/validate.go @@ -0,0 +1,45 @@ +// 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 v1_4 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnWarn(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayoutLegacy) + } + } + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} diff --git a/butane/config/fcos/v1_4/validate_test.go b/butane/config/fcos/v1_4/validate_test.go new file mode 100644 index 000000000..811778386 --- /dev/null +++ b/butane/config/fcos/v1_4/validate_test.go @@ -0,0 +1,180 @@ +// 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 v1_4 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_4" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_3Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayoutLegacy, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + } + + 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 validation report") + }) + } +} diff --git a/butane/config/fcos/v1_5/schema.go b/butane/config/fcos/v1_5/schema.go new file mode 100644 index 000000000..053b01afb --- /dev/null +++ b/butane/config/fcos/v1_5/schema.go @@ -0,0 +1,51 @@ +// 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 v1_5 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_5" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Discard *bool `yaml:"discard"` + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} + +type Grub struct { + Users []GrubUser `yaml:"users"` +} + +type GrubUser struct { + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` +} diff --git a/butane/config/fcos/v1_5/translate.go b/butane/config/fcos/v1_5/translate.go new file mode 100644 index 000000000..973432577 --- /dev/null +++ b/butane/config/fcos/v1_5/translate.go @@ -0,0 +1,387 @@ +// 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 v1_5 + +import ( + "fmt" + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// 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) { + ret, ts, r := c.Config.ToIgn3_4Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + + } + } + } + + retp, tsp, rp := c.handleUserGrubCfg(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + return ret, 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 +} + +// ToIgn3_4Bytes translates from a v1.5 Butane config to a v3.4.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 ToIgn3_4Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + luksDevice := "/dev/disk/by-partlabel/root" + if wantMirror { + luksDevice = "/dev/md/md-root" + } + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Clevis + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func (c Config) handleUserGrubCfg(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + yamlPath := path.New("yaml", "grub", "users") + if len(c.Grub.Users) == 0 { + // No users + return rendered, ts, r + } + + // create boot filesystem + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, + types.Filesystem{ + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }) + + userCfgContent := []byte(buildGrubConfig(c.Grub)) + src, compression, err := baseutil.MakeDataURL(userCfgContent, nil, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return rendered, ts, r + } + + // Create user.cfg file and add it to rendered config + rendered.Storage.Files = append(rendered.Storage.Files, + types.File{ + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(src), + Compression: compression, + }, + }, + }, + }) + + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + return rendered, ts, r +} + +func buildGrubConfig(gb Grub) string { + // Process super users and corresponding passwords + allUsers := []string{} + cmds := []string{} + + for _, user := range gb.Users { + // We have already validated that user.Name and user.PasswordHash are non-empty + allUsers = append(allUsers, user.Name) + // Command for setting users password + cmds = append(cmds, fmt.Sprintf("password_pbkdf2 %s %s", user.Name, *user.PasswordHash)) + } + superUserCmd := fmt.Sprintf("set superusers=\"%s\"\n", strings.Join(allUsers, " ")) + return "# Generated by Butane\n\n" + superUserCmd + strings.Join(cmds, "\n") + "\n" +} diff --git a/butane/config/fcos/v1_5/translate_test.go b/butane/config/fcos/v1_5/translate_test.go new file mode 100644 index 000000000..88a277381 --- /dev/null +++ b/butane/config/fcos/v1_5/translate_test.go @@ -0,0 +1,1893 @@ +// 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 v1_5 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // LUKS, x86_64, with Tang set for offline provisioning + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "advertisement"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "advertisement")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} + +// TestTranslateGrub tests translating the Butane config Grub section. +func TestTranslateGrub(t *testing.T) { + singleUserExpectedConfig := `# Generated by Butane + +set superusers="root" +password_pbkdf2 root grub.pbkdf2.sha512.10000.874A958E526409... +` + singleUserURI, singleUserCompression := baseutil.CompressDataURL(t, []byte(singleUserExpectedConfig)) + + multiUserExpectedConfig := `# Generated by Butane + +set superusers="root1 root2" +password_pbkdf2 root1 grub.pbkdf2.sha512.10000.874A958E526409... +password_pbkdf2 root2 grub.pbkdf2.sha512.10000.874B829D126209... +` + multiUserURI, multiUserCompression := baseutil.CompressDataURL(t, []byte(multiUserExpectedConfig)) + + // Some tests below have the same translations + translations := []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "compression")}, + } + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // config with 1 user + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(singleUserURI), + Compression: util.StrToPtr(singleUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + // config with 2 users (and 2 different hashes) + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root1", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + { + Name: "root2", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874B829D126209..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(multiUserURI), + Compression: util.StrToPtr(multiUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_5/validate.go b/butane/config/fcos/v1_5/validate.go new file mode 100644 index 000000000..2e9b6bc21 --- /dev/null +++ b/butane/config/fcos/v1_5/validate.go @@ -0,0 +1,57 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_5 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnWarn(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayoutLegacy) + } + } + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} + +func (user GrubUser) Validate(c path.ContextPath) (r report.Report) { + if user.Name == "" { + r.AddOnError(c.Append("name"), common.ErrGrubUserNameNotSpecified) + } + + if !util.NotEmpty(user.PasswordHash) { + r.AddOnError(c.Append("password_hash"), common.ErrGrubPasswordNotSpecified) + } + return +} diff --git a/butane/config/fcos/v1_5/validate_test.go b/butane/config/fcos/v1_5/validate_test.go new file mode 100644 index 000000000..84723bd2e --- /dev/null +++ b/butane/config/fcos/v1_5/validate_test.go @@ -0,0 +1,225 @@ +// 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 v1_5 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 5, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_4Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayoutLegacy, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + } + + 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 validation report") + }) + } +} + +func TestValidateGrubUser(t *testing.T) { + tests := []struct { + in GrubUser + out error + errPath path.ContextPath + }{ + // valid user + { + in: GrubUser{ + Name: "name", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: nil, + errPath: path.New("yaml"), + }, + // username is not specified + { + in: GrubUser{ + Name: "", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: common.ErrGrubUserNameNotSpecified, + errPath: path.New("yaml", "name"), + }, + // password is not specified + { + in: GrubUser{ + Name: "name", + }, + out: common.ErrGrubPasswordNotSpecified, + errPath: path.New("yaml", "password_hash"), + }, + } + + 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") + }) + } +} diff --git a/butane/config/fcos/v1_6/schema.go b/butane/config/fcos/v1_6/schema.go new file mode 100644 index 000000000..15b4ff6d7 --- /dev/null +++ b/butane/config/fcos/v1_6/schema.go @@ -0,0 +1,53 @@ +// 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 v1_6 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_6" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Cex base.Cex `yaml:"cex"` + Discard *bool `yaml:"discard"` + Device *string `yaml:"device"` + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} + +type Grub struct { + Users []GrubUser `yaml:"users"` +} + +type GrubUser struct { + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` +} diff --git a/butane/config/fcos/v1_6/translate.go b/butane/config/fcos/v1_6/translate.go new file mode 100644 index 000000000..504c25441 --- /dev/null +++ b/butane/config/fcos/v1_6/translate.go @@ -0,0 +1,431 @@ +// 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 v1_6 + +import ( + "fmt" + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_5Unvalidated 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_5Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_5Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + + } + } + } + + retp, tsp, rp := c.handleUserGrubCfg(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + return ret, ts, r +} + +// ToIgn3_5 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_5(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_5Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_5Bytes translates from a v1.6 Butane config to a v3.5.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 ToIgn3_5Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_5", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 || util.IsTrue(c.BootDevice.Luks.Cex.Enabled) + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + case *layout == "s390x-eckd" || *layout == "s390x-virt" || *layout == "s390x-zfcp": + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + var luksDevice string + switch { + //Luks Device for dasd and zFCP-scsi + case layout != nil && *layout == "s390x-eckd": + luksDevice = *c.BootDevice.Luks.Device + "2" + case layout != nil && *layout == "s390x-zfcp": + luksDevice = *c.BootDevice.Luks.Device + "4" + case wantMirror: + luksDevice = "/dev/md/md-root" + default: + luksDevice = "/dev/disk/by-partlabel/root" + } + if util.IsTrue(c.BootDevice.Luks.Cex.Enabled) { + cex, ts2, r2 := translateBootDeviceLuksCex(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Cex: cex, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("cex"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } else { + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Clevis + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func translateBootDeviceLuksCex(from BootDeviceLuks, options common.TranslateOptions) (to types.Cex, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Cex + tm, r = translate.Prefixed(tr, "enabled", &from.Cex.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "enabled", &from.Cex.Enabled, &to.Enabled) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func (c Config) handleUserGrubCfg(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + yamlPath := path.New("yaml", "grub", "users") + if len(c.Grub.Users) == 0 { + // No users + return rendered, ts, r + } + + // create boot filesystem + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, + types.Filesystem{ + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }) + + userCfgContent := []byte(buildGrubConfig(c.Grub)) + src, compression, err := baseutil.MakeDataURL(userCfgContent, nil, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return rendered, ts, r + } + + // Create user.cfg file and add it to rendered config + rendered.Storage.Files = append(rendered.Storage.Files, + types.File{ + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(src), + Compression: compression, + }, + }, + }, + }) + + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + return rendered, ts, r +} + +func buildGrubConfig(gb Grub) string { + // Process super users and corresponding passwords + allUsers := []string{} + cmds := []string{} + + for _, user := range gb.Users { + // We have already validated that user.Name and user.PasswordHash are non-empty + allUsers = append(allUsers, user.Name) + // Command for setting users password + cmds = append(cmds, fmt.Sprintf("password_pbkdf2 %s %s", user.Name, *user.PasswordHash)) + } + superUserCmd := fmt.Sprintf("set superusers=\"%s\"\n", strings.Join(allUsers, " ")) + return "# Generated by Butane\n\n" + superUserCmd + strings.Join(cmds, "\n") + "\n" +} diff --git a/butane/config/fcos/v1_6/translate_test.go b/butane/config/fcos/v1_6/translate_test.go new file mode 100644 index 000000000..1891f254d --- /dev/null +++ b/butane/config/fcos/v1_6/translate_test.go @@ -0,0 +1,1893 @@ +// 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 v1_6 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // LUKS, x86_64, with Tang set for offline provisioning + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "advertisement"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "advertisement")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_5Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_5Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} + +// TestTranslateGrub tests translating the Butane config Grub section. +func TestTranslateGrub(t *testing.T) { + singleUserExpectedConfig := `# Generated by Butane + +set superusers="root" +password_pbkdf2 root grub.pbkdf2.sha512.10000.874A958E526409... +` + singleUserURI, singleUserCompression := baseutil.CompressDataURL(t, []byte(singleUserExpectedConfig)) + + multiUserExpectedConfig := `# Generated by Butane + +set superusers="root1 root2" +password_pbkdf2 root1 grub.pbkdf2.sha512.10000.874A958E526409... +password_pbkdf2 root2 grub.pbkdf2.sha512.10000.874B829D126209... +` + multiUserURI, multiUserCompression := baseutil.CompressDataURL(t, []byte(multiUserExpectedConfig)) + + // Some tests below have the same translations + translations := []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "compression")}, + } + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // config with 1 user + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(singleUserURI), + Compression: util.StrToPtr(singleUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + // config with 2 users (and 2 different hashes) + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root1", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + { + Name: "root2", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874B829D126209..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.5.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(multiUserURI), + Compression: util.StrToPtr(multiUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_5Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_6/validate.go b/butane/config/fcos/v1_6/validate.go new file mode 100644 index 000000000..e43867edb --- /dev/null +++ b/butane/config/fcos/v1_6/validate.go @@ -0,0 +1,125 @@ +// 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 v1_6 + +import ( + "regexp" + "strings" + + "github.com/coreos/ignition/v2/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" +) + +const rootDevice = "/dev/disk/by-id/coreos-boot-disk" + +var allowedMountpoints = regexp.MustCompile(`^/(etc|var)(/|$)`) +var dasdRe = regexp.MustCompile("(/dev/dasd[a-z]$)") +var sdRe = regexp.MustCompile("(/dev/sd[a-z]$)") + +// We can't define a Validate function directly on Disk because that's defined in base, +// so we use a Validate function on the top-level Config instead. +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + // Collect mirror device paths so we can skip the reuse-by-label + // check for them; processBootDevice() will set wipe_table: true. + mirrorDevices := make(map[string]bool) + for _, dev := range conf.BootDevice.Mirror.Devices { + mirrorDevices[dev] = true + } + for i, disk := range conf.Storage.Disks { + if disk.Device != rootDevice && !util.IsTrue(disk.WipeTable) && !mirrorDevices[disk.Device] { + for p, partition := range disk.Partitions { + if partition.Number == 0 && partition.Label != nil { + r.AddOnWarn(c.Append("storage", "disks", i, "partitions", p, "number"), common.ErrReuseByLabel) + } + } + } + } + for i, fs := range conf.Storage.Filesystems { + if fs.Path != nil && !allowedMountpoints.MatchString(*fs.Path) && util.IsTrue(fs.WithMountUnit) { + r.AddOnError(c.Append("storage", "filesystems", i, "path"), common.ErrMountPointForbidden) + } + } + return +} + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnWarn(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + // Nothing to do + case "s390x-eckd": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !dasdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-zfcp": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !sdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-virt": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayout) + } + + // Mirroring the boot disk is not supported on s390x + if strings.HasPrefix(*d.Layout, "s390x") && len(d.Mirror.Devices) > 0 { + r.AddOnError(c.Append("layout"), common.ErrMirrorNotSupport) + } + } + + // CEX is only valid on s390x and incompatible with Clevis + if util.IsTrue(d.Luks.Cex.Enabled) { + if d.Layout == nil { + r.AddOnError(c.Append("luks", "cex"), common.ErrCexArchitectureMismatch) + } else if !strings.HasPrefix(*d.Layout, "s390x") { + r.AddOnError(c.Append("layout"), common.ErrCexArchitectureMismatch) + } + if len(d.Luks.Tang) > 0 || util.IsTrue(d.Luks.Tpm2) { + r.AddOnError(c.Append("luks"), errors.ErrCexWithClevis) + } + } + + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} + +func (user GrubUser) Validate(c path.ContextPath) (r report.Report) { + if user.Name == "" { + r.AddOnError(c.Append("name"), common.ErrGrubUserNameNotSpecified) + } + + if !util.NotEmpty(user.PasswordHash) { + r.AddOnError(c.Append("password_hash"), common.ErrGrubPasswordNotSpecified) + } + return +} diff --git a/butane/config/fcos/v1_6/validate_test.go b/butane/config/fcos/v1_6/validate_test.go new file mode 100644 index 000000000..40b3b0a0c --- /dev/null +++ b/butane/config/fcos/v1_6/validate_test.go @@ -0,0 +1,655 @@ +// 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 v1_6 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 6, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_5Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // complete config with cex + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + nil, + path.New("yaml"), + }, + // can not use both cex & tang + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use both cex & tpm2 + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tpm2: util.BoolToPtr(true), + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use cex on non s390x + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "layout"), + }, + // must set s390x layout with cex + { + BootDevice{ + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "luks", "cex"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayout, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + // s390x-eckd/s390x-zfcp layouts require a boot device with luks + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + }, + common.ErrNoLuksBootDevice, + path.New("yaml", "layout"), + }, + // s390x-eckd/s390x-zfcp layouts do not support mirroring + { + BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{ + "/dev/sda", + "/dev/sdb", + }, + }, + }, + common.ErrMirrorNotSupport, + path.New("yaml", "layout"), + }, + // s390x-eckd devices must start with /dev/dasd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // s390x-zfcp devices must start with /dev/sd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasd"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // mirror with layout should succeed + { + BootDevice{ + Layout: util.StrToPtr("aarch64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + } + + 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 validation report") + }) + } + + warningTests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // mirror without layout + { + BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + common.ErrMirrorRequiresLayout, + path.New("yaml", "mirror"), + }, + } + + for i, test := range warningTests { + t.Run(fmt.Sprintf("validate warning %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad validation report") + }) + } +} + +func TestValidateGrubUser(t *testing.T) { + tests := []struct { + in GrubUser + out error + errPath path.ContextPath + }{ + // valid user + { + in: GrubUser{ + Name: "name", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: nil, + errPath: path.New("yaml"), + }, + // username is not specified + { + in: GrubUser{ + Name: "", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: common.ErrGrubUserNameNotSpecified, + errPath: path.New("yaml", "name"), + }, + // password is not specified + { + in: GrubUser{ + Name: "name", + }, + out: common.ErrGrubPasswordNotSpecified, + errPath: path.New("yaml", "password_hash"), + }, + } + + 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 TestValidateMountPoints(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (has prefix "/etc" or "/var") + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/etc/foo"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/var"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/invalid/path"), + WithMountUnit: util.BoolToPtr(false), + }, + { + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: nil, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + // invalid config (path name is '/') + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /boot) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/boot"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is invalid, does not contain /etc or /var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/thisIsABugTest"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /varnish) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/varnish"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /foo/var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/foo/var"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + } + 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, "invalid report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (wipe_table is true) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(true), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is /dev/disk/by-id/coreos-boot-disk) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: rootDevice, + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is a boot_device.mirror device) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/sda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + }, + }, + }, + }, + }, + }, + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/sda", "/dev/sdb"}, + }, + }, + }, + }, + // invalid config (wipe_table is nil) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + // invalid config (wipe_table is false with a partition numbered 0) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + { + Label: util.StrToPtr("bar"), + Number: 2, + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + } + + 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.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "invalid report") + }) + } +} diff --git a/butane/config/fcos/v1_7/schema.go b/butane/config/fcos/v1_7/schema.go new file mode 100644 index 000000000..1b31c7d4f --- /dev/null +++ b/butane/config/fcos/v1_7/schema.go @@ -0,0 +1,53 @@ +// 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 v1_7 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_7" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Cex base.Cex `yaml:"cex"` + Discard *bool `yaml:"discard"` + Device *string `yaml:"device"` + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} + +type Grub struct { + Users []GrubUser `yaml:"users"` +} + +type GrubUser struct { + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` +} diff --git a/butane/config/fcos/v1_7/translate.go b/butane/config/fcos/v1_7/translate.go new file mode 100644 index 000000000..51d50e86d --- /dev/null +++ b/butane/config/fcos/v1_7/translate.go @@ -0,0 +1,431 @@ +// 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 v1_7 + +import ( + "fmt" + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// 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) { + ret, ts, r := c.Config.ToIgn3_6Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + + } + } + } + + retp, tsp, rp := c.handleUserGrubCfg(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + return ret, 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 +} + +// ToIgn3_6Bytes translates from a v1.6 Butane config to a v3.5.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 ToIgn3_6Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 || util.IsTrue(c.BootDevice.Luks.Cex.Enabled) + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + case *layout == "s390x-eckd" || *layout == "s390x-virt" || *layout == "s390x-zfcp": + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + var luksDevice string + switch { + //Luks Device for dasd and zFCP-scsi + case layout != nil && *layout == "s390x-eckd": + luksDevice = *c.BootDevice.Luks.Device + "2" + case layout != nil && *layout == "s390x-zfcp": + luksDevice = *c.BootDevice.Luks.Device + "4" + case wantMirror: + luksDevice = "/dev/md/md-root" + default: + luksDevice = "/dev/disk/by-partlabel/root" + } + if util.IsTrue(c.BootDevice.Luks.Cex.Enabled) { + cex, ts2, r2 := translateBootDeviceLuksCex(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Cex: cex, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("cex"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } else { + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Clevis + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func translateBootDeviceLuksCex(from BootDeviceLuks, options common.TranslateOptions) (to types.Cex, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Cex + tm, r = translate.Prefixed(tr, "enabled", &from.Cex.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "enabled", &from.Cex.Enabled, &to.Enabled) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func (c Config) handleUserGrubCfg(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + yamlPath := path.New("yaml", "grub", "users") + if len(c.Grub.Users) == 0 { + // No users + return rendered, ts, r + } + + // create boot filesystem + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, + types.Filesystem{ + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }) + + userCfgContent := []byte(buildGrubConfig(c.Grub)) + src, compression, err := baseutil.MakeDataURL(userCfgContent, nil, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return rendered, ts, r + } + + // Create user.cfg file and add it to rendered config + rendered.Storage.Files = append(rendered.Storage.Files, + types.File{ + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(src), + Compression: compression, + }, + }, + }, + }) + + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + return rendered, ts, r +} + +func buildGrubConfig(gb Grub) string { + // Process super users and corresponding passwords + allUsers := []string{} + cmds := []string{} + + for _, user := range gb.Users { + // We have already validated that user.Name and user.PasswordHash are non-empty + allUsers = append(allUsers, user.Name) + // Command for setting users password + cmds = append(cmds, fmt.Sprintf("password_pbkdf2 %s %s", user.Name, *user.PasswordHash)) + } + superUserCmd := fmt.Sprintf("set superusers=\"%s\"\n", strings.Join(allUsers, " ")) + return "# Generated by Butane\n\n" + superUserCmd + strings.Join(cmds, "\n") + "\n" +} diff --git a/butane/config/fcos/v1_7/translate_test.go b/butane/config/fcos/v1_7/translate_test.go new file mode 100644 index 000000000..8b35f70b4 --- /dev/null +++ b/butane/config/fcos/v1_7/translate_test.go @@ -0,0 +1,1894 @@ +// 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 v1_7 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_7" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // LUKS, x86_64, with Tang set for offline provisioning + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "advertisement"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "advertisement")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition too small (explicit size < 8192 MiB) + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} + +// TestTranslateGrub tests translating the Butane config Grub section. +func TestTranslateGrub(t *testing.T) { + singleUserExpectedConfig := `# Generated by Butane + +set superusers="root" +password_pbkdf2 root grub.pbkdf2.sha512.10000.874A958E526409... +` + singleUserURI, singleUserCompression := baseutil.CompressDataURL(t, []byte(singleUserExpectedConfig)) + + multiUserExpectedConfig := `# Generated by Butane + +set superusers="root1 root2" +password_pbkdf2 root1 grub.pbkdf2.sha512.10000.874A958E526409... +password_pbkdf2 root2 grub.pbkdf2.sha512.10000.874B829D126209... +` + multiUserURI, multiUserCompression := baseutil.CompressDataURL(t, []byte(multiUserExpectedConfig)) + + // Some tests below have the same translations + translations := []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "compression")}, + } + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // config with 1 user + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(singleUserURI), + Compression: util.StrToPtr(singleUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + // config with 2 users (and 2 different hashes) + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root1", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + { + Name: "root2", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874B829D126209..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(multiUserURI), + Compression: util.StrToPtr(multiUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/fcos/v1_7/validate.go b/butane/config/fcos/v1_7/validate.go new file mode 100644 index 000000000..cd5cefab7 --- /dev/null +++ b/butane/config/fcos/v1_7/validate.go @@ -0,0 +1,146 @@ +// 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 v1_7 + +import ( + "regexp" + "strings" + + base "github.com/coreos/ignition/v2/butane/base/v0_7" + "github.com/coreos/ignition/v2/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" +) + +const rootDevice = "/dev/disk/by-id/coreos-boot-disk" + +var allowedMountpoints = regexp.MustCompile(`^/(etc|var)(/|$)`) +var dasdRe = regexp.MustCompile("(/dev/dasd[a-z]$)") +var sdRe = regexp.MustCompile("(/dev/sd[a-z]$)") + +// We can't define a Validate function directly on Disk because that's defined in base, +// so we use a Validate function on the top-level Config instead. +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + // Collect mirror device paths so we can skip the reuse-by-label + // check for them; processBootDevice() will set wipe_table: true. + mirrorDevices := make(map[string]bool) + for _, dev := range conf.BootDevice.Mirror.Devices { + mirrorDevices[dev] = true + } + for i, disk := range conf.Storage.Disks { + if disk.Device != rootDevice && !util.IsTrue(disk.WipeTable) && !mirrorDevices[disk.Device] { + for p, partition := range disk.Partitions { + if partition.Number == 0 && partition.Label != nil { + r.AddOnWarn(c.Append("storage", "disks", i, "partitions", p, "number"), common.ErrReuseByLabel) + } + } + } + } + for i, fs := range conf.Storage.Filesystems { + if fs.Path != nil && !allowedMountpoints.MatchString(*fs.Path) && util.IsTrue(fs.WithMountUnit) { + r.AddOnError(c.Append("storage", "filesystems", i, "path"), common.ErrMountPointForbidden) + } + } + return +} + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnError(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + // Nothing to do + case "s390x-eckd": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !dasdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-zfcp": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !sdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-virt": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayout) + } + + // Mirroring the boot disk is not supported on s390x + if strings.HasPrefix(*d.Layout, "s390x") && len(d.Mirror.Devices) > 0 { + r.AddOnError(c.Append("layout"), common.ErrMirrorNotSupport) + } + } + + // CEX is only valid on s390x and incompatible with Clevis + if util.IsTrue(d.Luks.Cex.Enabled) { + if d.Layout == nil { + r.AddOnError(c.Append("luks", "cex"), common.ErrCexArchitectureMismatch) + } else if !strings.HasPrefix(*d.Layout, "s390x") { + r.AddOnError(c.Append("layout"), common.ErrCexArchitectureMismatch) + } + if len(d.Luks.Tang) > 0 || util.IsTrue(d.Luks.Tpm2) { + r.AddOnError(c.Append("luks"), errors.ErrCexWithClevis) + } + } + + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (l BootDeviceLuks) Validate(c path.ContextPath) (r report.Report) { + if util.NotEmpty(l.Device) { + valid := false + for _, t := range l.Tang { + if t != (base.Tang{}) { + valid = true + } + } + if util.IsTrue(l.Tpm2) { + valid = true + } else if util.IsTrue(l.Cex.Enabled) { + valid = true + } + if !valid { + r.AddOnError(c.Append("luks"), common.ErrNoLuksMethodSpecified) + } + } + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} + +func (user GrubUser) Validate(c path.ContextPath) (r report.Report) { + if user.Name == "" { + r.AddOnError(c.Append("name"), common.ErrGrubUserNameNotSpecified) + } + + if !util.NotEmpty(user.PasswordHash) { + r.AddOnError(c.Append("password_hash"), common.ErrGrubPasswordNotSpecified) + } + return +} diff --git a/butane/config/fcos/v1_7/validate_test.go b/butane/config/fcos/v1_7/validate_test.go new file mode 100644 index 000000000..807d22e06 --- /dev/null +++ b/butane/config/fcos/v1_7/validate_test.go @@ -0,0 +1,638 @@ +// 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 v1_7 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_7" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 6, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_6Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // complete config with cex + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + nil, + path.New("yaml"), + }, + // can not use both cex & tang + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use both cex & tpm2 + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tpm2: util.BoolToPtr(true), + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use cex on non s390x + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "layout"), + }, + // must set s390x layout with cex + { + BootDevice{ + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "luks", "cex"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayout, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + // s390x-eckd/s390x-zfcp layouts require a boot device with luks + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + }, + common.ErrNoLuksBootDevice, + path.New("yaml", "layout"), + }, + // s390x-eckd/s390x-zfcp layouts do not support mirroring + { + BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{ + "/dev/sda", + "/dev/sdb", + }, + }, + }, + common.ErrMirrorNotSupport, + path.New("yaml", "layout"), + }, + // s390x-eckd devices must start with /dev/dasd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // s390x-zfcp devices must start with /dev/sd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasd"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // mirror with layout should succeed + { + BootDevice{ + Layout: util.StrToPtr("aarch64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // mirror without layout + { + BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + common.ErrMirrorRequiresLayout, + path.New("yaml", "mirror"), + }, + } + + 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 validation report") + }) + } +} + +func TestValidateGrubUser(t *testing.T) { + tests := []struct { + in GrubUser + out error + errPath path.ContextPath + }{ + // valid user + { + in: GrubUser{ + Name: "name", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: nil, + errPath: path.New("yaml"), + }, + // username is not specified + { + in: GrubUser{ + Name: "", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: common.ErrGrubUserNameNotSpecified, + errPath: path.New("yaml", "name"), + }, + // password is not specified + { + in: GrubUser{ + Name: "name", + }, + out: common.ErrGrubPasswordNotSpecified, + errPath: path.New("yaml", "password_hash"), + }, + } + + 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 TestValidateMountPoints(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (has prefix "/etc" or "/var") + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/etc/foo"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/var"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/invalid/path"), + WithMountUnit: util.BoolToPtr(false), + }, + { + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: nil, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + // invalid config (path name is '/') + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /boot) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/boot"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is invalid, does not contain /etc or /var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/thisIsABugTest"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /varnish) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/varnish"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /foo/var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/foo/var"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + } + 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, "invalid report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (wipe_table is true) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(true), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is /dev/disk/by-id/coreos-boot-disk) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: rootDevice, + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is a boot_device.mirror device) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/sda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + }, + }, + }, + }, + }, + }, + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/sda", "/dev/sdb"}, + }, + }, + }, + }, + // invalid config (wipe_table is nil) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + // invalid config (wipe_table is false with a partition numbered 0) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + { + Label: util.StrToPtr("bar"), + Number: 2, + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + } + + 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.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "invalid report") + }) + } +} diff --git a/butane/config/fcos/v1_8_exp/schema.go b/butane/config/fcos/v1_8_exp/schema.go new file mode 100644 index 000000000..947be98de --- /dev/null +++ b/butane/config/fcos/v1_8_exp/schema.go @@ -0,0 +1,53 @@ +// 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 v1_8_exp + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" +) + +type Config struct { + base.Config `yaml:",inline"` + BootDevice BootDevice `yaml:"boot_device"` + Grub Grub `yaml:"grub"` +} + +type BootDevice struct { + Layout *string `yaml:"layout"` + Luks BootDeviceLuks `yaml:"luks"` + Mirror BootDeviceMirror `yaml:"mirror"` +} + +type BootDeviceLuks struct { + Cex base.Cex `yaml:"cex"` + Discard *bool `yaml:"discard"` + Device *string `yaml:"device"` + Tang []base.Tang `yaml:"tang"` + Threshold *int `yaml:"threshold"` + Tpm2 *bool `yaml:"tpm2"` +} + +type BootDeviceMirror struct { + Devices []string `yaml:"devices"` +} + +type Grub struct { + Users []GrubUser `yaml:"users"` +} + +type GrubUser struct { + Name string `yaml:"name"` + PasswordHash *string `yaml:"password_hash"` +} diff --git a/butane/config/fcos/v1_8_exp/translate.go b/butane/config/fcos/v1_8_exp/translate.go new file mode 100644 index 000000000..bdbc202e8 --- /dev/null +++ b/butane/config/fcos/v1_8_exp/translate.go @@ -0,0 +1,430 @@ +// 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 v1_8_exp + +import ( + "fmt" + "strings" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +const ( + reservedTypeGuid = "8DA63339-0007-60C0-C436-083AC8230908" + biosTypeGuid = "21686148-6449-6E6F-744E-656564454649" + prepTypeGuid = "9E1A2D38-C612-4316-AA26-8B49521E5A8B" + espTypeGuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + // The partition layout implemented in this file replicates + // the layout of the OS image defined in: + // https://github.com/coreos/coreos-assembler/blob/main/src/create_disk.sh + // + // It's not critical that we match that layout exactly; the hard + // constraints are: + // - The desugared partition cannot be smaller than the one it + // replicates + // - The new BIOS-BOOT partition (and maybe the PReP one?) must be + // at the same offset as the original + // + // Do not change these constants! New partition layouts must be + // encoded into new layout templates. + reservedV1SizeMiB = 1 + biosV1SizeMiB = 1 + prepV1SizeMiB = 4 + espV1SizeMiB = 127 + bootV1SizeMiB = 384 +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return nil +} + +// ToIgn3_7Unvalidated 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_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + ret, ts, r := c.Config.ToIgn3_7Unvalidated(options) + if r.IsFatal() { + return types.Config{}, translate.TranslationSet{}, r + } + r.Merge(c.processBootDevice(&ret, &ts, options)) + for i, disk := range ret.Storage.Disks { + for p, partition := range disk.Partitions { + // check for root partition size constraints + if partition.Label != nil { + if *partition.Label == "root" { + if partition.SizeMiB == nil || *partition.SizeMiB == 0 { + for idx := range disk.Partitions { + if idx == p { + continue + } + if disk.Partitions[idx].StartMiB == nil || *disk.Partitions[idx].StartMiB == 0 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrRootConstrained) + break + } + } + } else if *partition.SizeMiB < 8192 { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "size_mib"), common.ErrRootTooSmall) + } + } + + // In the boot_device.mirror case, nothing specifies partition numbers + // so match existing partitions only when `wipeTable` is false + if !util.IsTrue(disk.WipeTable) { + // check for reserved partlabels + if (*partition.Label == "BIOS-BOOT" && partition.Number != 1) || (*partition.Label == "PowerPC-PReP-boot" && partition.Number != 1) || (*partition.Label == "EFI-SYSTEM" && partition.Number != 2) || (*partition.Label == "boot" && partition.Number != 3) || (*partition.Label == "root" && partition.Number != 4) { + r.AddOnWarn(path.New("json", "storage", "disks", i, "partitions", p, "label"), common.ErrWrongPartitionNumber) + } + } + } + } + } + + retp, tsp, rp := c.handleUserGrubCfg(options) + retConfig, ts := baseutil.MergeTranslatedConfigs(retp, tsp, ret, ts) + ret = retConfig.(types.Config) + r.Merge(rp) + return ret, ts, r +} + +// ToIgn3_7 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_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_7Bytes translates from a v1.6 Butane config to a v3.5.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 ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) +} + +func (c Config) processBootDevice(config *types.Config, ts *translate.TranslationSet, options common.TranslateOptions) report.Report { + var rendered types.Config + renderedTranslations := translate.NewTranslationSet("yaml", "json") + var r report.Report + + // check for high-level features + wantLuks := util.IsTrue(c.BootDevice.Luks.Tpm2) || len(c.BootDevice.Luks.Tang) > 0 || util.IsTrue(c.BootDevice.Luks.Cex.Enabled) + wantMirror := len(c.BootDevice.Mirror.Devices) > 0 + if !wantLuks && !wantMirror { + return r + } + + // compute layout rendering options + var wantBIOSPart bool + var wantEFIPart bool + var wantPRePPart bool + layout := c.BootDevice.Layout + switch { + case layout == nil || *layout == "x86_64": + wantBIOSPart = true + wantEFIPart = true + case *layout == "aarch64": + wantEFIPart = true + case *layout == "ppc64le": + wantPRePPart = true + case *layout == "s390x-eckd" || *layout == "s390x-virt" || *layout == "s390x-zfcp": + default: + // should have failed validation + panic("unknown layout") + } + + // mirrored root disk + if wantMirror { + // partition disks + for i, device := range c.BootDevice.Mirror.Devices { + labelIndex := len(rendered.Storage.Disks) + 1 + disk := types.Disk{ + Device: device, + WipeTable: util.BoolToPtr(true), + } + if wantBIOSPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("bios-%d", labelIndex)), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }) + } else if wantPRePPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("prep-%d", labelIndex)), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + if wantEFIPart { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }) + } else { + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("reserved-%d", labelIndex)), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }) + } + disk.Partitions = append(disk.Partitions, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("boot-%d", labelIndex)), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, types.Partition{ + Label: util.StrToPtr(fmt.Sprintf("root-%d", labelIndex)), + }) + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "disks", len(rendered.Storage.Disks)), disk) + rendered.Storage.Disks = append(rendered.Storage.Disks, disk) + + if wantEFIPart { + // add ESP filesystem + espFilesystem := types.Filesystem{ + Device: fmt.Sprintf("/dev/disk/by-partlabel/esp-%d", labelIndex), + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr(fmt.Sprintf("esp-%d", labelIndex)), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror", "devices", i), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), espFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, espFilesystem) + } + } + renderedTranslations.AddTranslation(path.New("yaml", "boot_device", "mirror", "devices"), path.New("json", "storage", "disks")) + + // create RAIDs + raidDevices := func(labelPrefix string) []types.Device { + count := len(rendered.Storage.Disks) + ret := make([]types.Device, count) + for i := 0; i < count; i++ { + ret[i] = types.Device(fmt.Sprintf("/dev/disk/by-partlabel/%s-%d", labelPrefix, i+1)) + } + return ret + } + rendered.Storage.Raid = []types.Raid{{ + Devices: raidDevices("boot"), + Level: util.StrToPtr("raid1"), + Name: "md-boot", + // put the RAID superblock at the end of the + // partition so BIOS GRUB doesn't need to + // understand RAID + Options: []types.RaidOption{"--metadata=1.0"}, + }, { + Devices: raidDevices("root"), + Level: util.StrToPtr("raid1"), + Name: "md-root", + }} + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "raid"), rendered.Storage.Raid) + + // create boot filesystem + bootFilesystem := types.Filesystem{ + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device", "mirror"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), bootFilesystem) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, bootFilesystem) + } + + // encrypted root partition + if wantLuks { + var luksDevice string + switch { + //Luks Device for dasd and zFCP-scsi + case layout != nil && *layout == "s390x-eckd": + luksDevice = *c.BootDevice.Luks.Device + "2" + case layout != nil && *layout == "s390x-zfcp": + luksDevice = *c.BootDevice.Luks.Device + "4" + case wantMirror: + luksDevice = "/dev/md/md-root" + default: + luksDevice = "/dev/disk/by-partlabel/root" + } + if util.IsTrue(c.BootDevice.Luks.Cex.Enabled) { + cex, ts2, r2 := translateBootDeviceLuksCex(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Cex: cex, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("cex"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } else { + clevis, ts2, r2 := translateBootDeviceLuks(c.BootDevice.Luks, options) + rendered.Storage.Luks = []types.Luks{{ + Clevis: clevis, + Device: &luksDevice, + Discard: c.BootDevice.Luks.Discard, + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }} + lpath := path.New("yaml", "boot_device", "luks") + rpath := path.New("json", "storage", "luks", 0) + renderedTranslations.Merge(ts2.PrefixPaths(lpath, rpath.Append("clevis"))) + renderedTranslations.AddTranslation(lpath.Append("discard"), rpath.Append("discard")) + for _, f := range []string{"device", "label", "name", "wipeVolume"} { + renderedTranslations.AddTranslation(lpath, rpath.Append(f)) + } + renderedTranslations.AddTranslation(lpath, rpath) + renderedTranslations.AddTranslation(lpath, path.New("json", "storage", "luks")) + r.Merge(r2) + } + } + + // create root filesystem + var rootDevice string + switch { + case wantLuks: + // LUKS, or LUKS on RAID + rootDevice = "/dev/mapper/root" + case wantMirror: + // RAID without LUKS + rootDevice = "/dev/md/md-root" + default: + panic("can't happen") + } + rootFilesystem := types.Filesystem{ + Device: rootDevice, + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + } + renderedTranslations.AddFromCommonSource(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems", len(rendered.Storage.Filesystems)), rootFilesystem) + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage", "filesystems")) + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, rootFilesystem) + + // merge with translated config + renderedTranslations.AddTranslation(path.New("yaml", "boot_device"), path.New("json", "storage")) + retConfig, retTranslations := baseutil.MergeTranslatedConfigs(rendered, renderedTranslations, *config, *ts) + *config = retConfig.(types.Config) + *ts = retTranslations + return r +} + +func translateBootDeviceLuks(from BootDeviceLuks, options common.TranslateOptions) (to types.Clevis, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Clevis + tm, r = translate.Prefixed(tr, "tang", &from.Tang, &to.Tang) + translate.MergeP(tr, tm, &r, "threshold", &from.Threshold, &to.Threshold) + translate.MergeP(tr, tm, &r, "tpm2", &from.Tpm2, &to.Tpm2) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func translateBootDeviceLuksCex(from BootDeviceLuks, options common.TranslateOptions) (to types.Cex, tm translate.TranslationSet, r report.Report) { + tr := translate.NewTranslator("yaml", "json", options) + // Discard field is handled by the caller because it doesn't go + // into types.Cex + tm, r = translate.Prefixed(tr, "enabled", &from.Cex.Enabled, &to.Enabled) + translate.MergeP(tr, tm, &r, "enabled", &from.Cex.Enabled, &to.Enabled) + // we're being called manually, not via the translate package's + // custom translator mechanism, so we have to add the base + // translation ourselves + tm.AddTranslation(path.New("yaml"), path.New("json")) + return +} + +func (c Config) handleUserGrubCfg(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + rendered := types.Config{} + ts := translate.NewTranslationSet("yaml", "json") + var r report.Report + yamlPath := path.New("yaml", "grub", "users") + if len(c.Grub.Users) == 0 { + // No users + return rendered, ts, r + } + + // create boot filesystem + rendered.Storage.Filesystems = append(rendered.Storage.Filesystems, + types.Filesystem{ + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }) + + userCfgContent := []byte(buildGrubConfig(c.Grub)) + src, compression, err := baseutil.MakeDataURL(userCfgContent, nil, !options.NoResourceAutoCompression) + if err != nil { + r.AddOnError(yamlPath, err) + return rendered, ts, r + } + + // Create user.cfg file and add it to rendered config + rendered.Storage.Files = append(rendered.Storage.Files, + types.File{ + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(src), + Compression: compression, + }, + }, + }, + }) + + ts.AddFromCommonSource(yamlPath, path.New("json", "storage"), rendered.Storage) + return rendered, ts, r +} + +func buildGrubConfig(gb Grub) string { + // Process super users and corresponding passwords + allUsers := []string{} + cmds := []string{} + + for _, user := range gb.Users { + // We have already validated that user.Name and user.PasswordHash are non-empty + allUsers = append(allUsers, user.Name) + // Command for setting users password + cmds = append(cmds, fmt.Sprintf("password_pbkdf2 %s %s", user.Name, *user.PasswordHash)) + } + superUserCmd := fmt.Sprintf("set superusers=\"%s\"\n", strings.Join(allUsers, " ")) + return "# Generated by Butane\n\n" + superUserCmd + strings.Join(cmds, "\n") + "\n" +} diff --git a/butane/config/fcos/v1_8_exp/translate_test.go b/butane/config/fcos/v1_8_exp/translate_test.go new file mode 100644 index 000000000..fceab29d9 --- /dev/null +++ b/butane/config/fcos/v1_8_exp/translate_test.go @@ -0,0 +1,1893 @@ +// 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 v1_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Most of this is covered by the Ignition translator generic tests, so just test the custom bits + +// TestTranslateBootDevice tests translating the Butane config boot_device section. +func TestTranslateBootDevice(t *testing.T) { + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // empty config + { + Config{}, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + }, + report.Report{}, + }, + // partition number for the `root` label is incorrect + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("root"), + SizeMiB: util.IntToPtr(12000), + Resize: util.BoolToPtr(true), + }, + { + Label: util.StrToPtr("var-home"), + SizeMiB: util.IntToPtr(10240), + }, + }, + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/var-home", + Format: util.StrToPtr("xfs"), + Path: util.StrToPtr("/var/home"), + Label: util.StrToPtr("var-home"), + WipeFilesystem: util.BoolToPtr(false), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "resize"), To: path.New("json", "storage", "disks", 0, "partitions", 0, "resize")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 1, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "storage", "filesystems", 0, "path"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "storage", "filesystems", 0, "label"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "storage", "filesystems", 0, "wipe_filesystem"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrWrongPartitionNumber.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // LUKS, x86_64, with Tang set for offline provisioning + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + Advertisement: util.StrToPtr("{\"payload\": \"xyzzy\"}"), + }}, + }, + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "advertisement"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "advertisement")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror, x86_64 + { + Config{ + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 3-disk mirror + LUKS, x86_64 + { + Config{ + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb", "/dev/vdc"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdc", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-3"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-3"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-3"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-3"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + "/dev/disk/by-partlabel/boot-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + "/dev/disk/by-partlabel/root-3", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-3", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-3"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "disks", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 2), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 2)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 4)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, aarch64 + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("aarch64"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS, ppc64le + { + Config{ + BootDevice: BootDevice{ + Layout: util.StrToPtr("ppc64le"), + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-1"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-1"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("prep-2"), + SizeMiB: util.IntToPtr(prepV1SizeMiB), + TypeGUID: util.StrToPtr(prepTypeGuid), + }, + { + Label: util.StrToPtr("reserved-2"), + SizeMiB: util.IntToPtr(reservedV1SizeMiB), + TypeGUID: util.StrToPtr(reservedTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices"), To: path.New("json", "storage", "disks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + // 2-disk mirror + LUKS with overridden root partition size + // and filesystem type, x86_64 + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + { + Device: "/dev/vdb", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + }, + }, + }, + }, + BootDevice: BootDevice{ + Luks: BootDeviceLuks{ + Discard: util.BoolToPtr(true), + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Disks: []types.Disk{ + { + Device: "/dev/vda", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-1"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-1"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-1"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-1"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + { + Device: "/dev/vdb", + Partitions: []types.Partition{ + { + Label: util.StrToPtr("bios-2"), + SizeMiB: util.IntToPtr(biosV1SizeMiB), + TypeGUID: util.StrToPtr(biosTypeGuid), + }, + { + Label: util.StrToPtr("esp-2"), + SizeMiB: util.IntToPtr(espV1SizeMiB), + TypeGUID: util.StrToPtr(espTypeGuid), + }, + { + Label: util.StrToPtr("boot-2"), + SizeMiB: util.IntToPtr(bootV1SizeMiB), + }, + { + Label: util.StrToPtr("root-2"), + SizeMiB: util.IntToPtr(8192), + }, + }, + WipeTable: util.BoolToPtr(true), + }, + }, + Raid: []types.Raid{ + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/boot-1", + "/dev/disk/by-partlabel/boot-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-boot", + Options: []types.RaidOption{"--metadata=1.0"}, + }, + { + Devices: []types.Device{ + "/dev/disk/by-partlabel/root-1", + "/dev/disk/by-partlabel/root-2", + }, + Level: util.StrToPtr("raid1"), + Name: "md-root", + }, + }, + Luks: []types.Luks{ + { + Clevis: types.Clevis{ + Tang: []types.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("z"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Device: util.StrToPtr("/dev/md/md-root"), + Discard: util.BoolToPtr(true), + Label: util.StrToPtr("luks-root"), + Name: "root", + WipeVolume: util.BoolToPtr(true), + }, + }, + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-partlabel/esp-1", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-1"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/disk/by-partlabel/esp-2", + Format: util.StrToPtr("vfat"), + Label: util.StrToPtr("esp-2"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/md/md-boot", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("boot"), + WipeFilesystem: util.BoolToPtr(true), + }, { + Device: "/dev/mapper/root", + Format: util.StrToPtr("ext4"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 0, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 0, "partitions", 0), To: path.New("json", "storage", "disks", 0, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "disks", 0, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 0), To: path.New("json", "storage", "disks", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 0), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 0)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1, "typeGuid")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2, "sizeMiB")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "partitions", 2)}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "label"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "label")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0, "size_mib"), To: path.New("json", "storage", "disks", 1, "partitions", 3, "sizeMiB")}, + {From: path.New("yaml", "storage", "disks", 1, "partitions", 0), To: path.New("json", "storage", "disks", 1, "partitions", 3)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "disks", 1, "wipeTable")}, + {From: path.New("yaml", "storage", "disks", 1), To: path.New("json", "storage", "disks", 1)}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "device")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "format")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "label")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror", "devices", 1), To: path.New("json", "storage", "filesystems", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0, "options")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 0)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "devices")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "level")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1, "name")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid", 1)}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "raid")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "url"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "url")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0, "thumbprint"), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0, "thumbprint")}, + {From: path.New("yaml", "boot_device", "luks", "tang", 0), To: path.New("json", "storage", "luks", 0, "clevis", "tang", 0)}, + {From: path.New("yaml", "boot_device", "luks", "tang"), To: path.New("json", "storage", "luks", 0, "clevis", "tang")}, + {From: path.New("yaml", "boot_device", "luks", "threshold"), To: path.New("json", "storage", "luks", 0, "clevis", "threshold")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks", "discard"), To: path.New("json", "storage", "luks", 0, "discard")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks", 0)}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "storage", "luks")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "device")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "format")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "label")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device", "mirror"), To: path.New("json", "storage", "filesystems", 2)}, + {From: path.New("yaml", "storage", "filesystems", 0, "device"), To: path.New("json", "storage", "filesystems", 3, "device")}, + {From: path.New("yaml", "storage", "filesystems", 0, "format"), To: path.New("json", "storage", "filesystems", 3, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "storage", "filesystems", 3, "wipeFilesystem")}, + {From: path.New("yaml", "storage", "filesystems", 0), To: path.New("json", "storage", "filesystems", 3)}, + {From: path.New("yaml", "storage", "filesystems"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "storage")}, + }, + report.Report{}, + }, + } + + // The partition sizes of existing layouts must never change, but + // we use the constants in tests for clarity. Ensure no one has + // changed them. + assert.Equal(t, reservedV1SizeMiB, 1) + assert.Equal(t, biosV1SizeMiB, 1) + assert.Equal(t, prepV1SizeMiB, 4) + assert.Equal(t, espV1SizeMiB, 127) + assert.Equal(t, bootV1SizeMiB, 384) + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// TestTranslateGrub tests translating the Butane config Grub section. +func TestTranslateGrub(t *testing.T) { + singleUserExpectedConfig := `# Generated by Butane + +set superusers="root" +password_pbkdf2 root grub.pbkdf2.sha512.10000.874A958E526409... +` + singleUserURI, singleUserCompression := baseutil.CompressDataURL(t, []byte(singleUserExpectedConfig)) + + multiUserExpectedConfig := `# Generated by Butane + +set superusers="root1 root2" +password_pbkdf2 root1 grub.pbkdf2.sha512.10000.874A958E526409... +password_pbkdf2 root2 grub.pbkdf2.sha512.10000.874B829D126209... +` + multiUserURI, multiUserCompression := baseutil.CompressDataURL(t, []byte(multiUserExpectedConfig)) + + // Some tests below have the same translations + translations := []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "ignition", "version")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "storage", "files", 0, "append", 0, "compression")}, + } + tests := []struct { + in Config + out types.Config + exceptions []translate.Translation + report report.Report + }{ + // config with 1 user + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(singleUserURI), + Compression: util.StrToPtr(singleUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + // config with 2 users (and 2 different hashes) + { + Config{ + Grub: Grub{ + Users: []GrubUser{ + { + Name: "root1", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + { + Name: "root2", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874B829D126209..."), + }, + }, + }, + }, + types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Append: []types.Resource{ + { + Source: util.StrToPtr(multiUserURI), + Compression: util.StrToPtr(multiUserCompression), + }, + }, + }, + }, + }, + }, + }, + translations, + report.Report{}, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, test.report, r, "report mismatch") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +func TestRootPartitionConstraints(t *testing.T) { + tests := []struct { + name string + in Config + report report.Report + }{ + { + name: "root constrained by auto-positioned partition", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(0), // auto-positioned - will be placed after root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root constrained by auto-positioned partition with explicit root start", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + // Root partition NOT constrained because next partition has explicit StartMiB + { + name: "root not constrained with explicit StartMiB after", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position - does NOT constrain root + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + // Root partition constrained by auto-positioned partition even when + // an explicit partition is also present + { + name: "root constrained by auto-positioned partition with explicit also present", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(0), // fill available + StartMiB: util.IntToPtr(2048), + }, + { + Label: util.StrToPtr("var"), + StartMiB: util.IntToPtr(0), // auto-positioned - constrains root + }, + { + Label: util.StrToPtr("data"), + StartMiB: util.IntToPtr(10240), // explicit position + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + { + name: "root partition too small", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(4096), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootTooSmall.Error(), + Context: path.New("json", "storage", "disks", 0, "partitions", 0, "size_mib"), + }, + }, + }, + }, + { + name: "root partition exactly 8GiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + SizeMiB: util.IntToPtr(8192), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{}, + }, + { + name: "root constrained with nil sizeMiB and nil startMiB", + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root"), + Number: 4, + }, + { + Label: util.StrToPtr("data"), + }, + }, + }, + }, + }, + }, + }, + report: report.Report{ + Entries: []report.Entry{ + { + Kind: report.Warn, + Message: common.ErrRootConstrained.Error(), + Context: path.New("yaml", "storage", "disks", 0, "partitions", 0, "label"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, translations, r := test.in.ToIgn3_7Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + assert.Equal(t, test.report, r, "report mismatch") + }) + } +} diff --git a/butane/config/fcos/v1_8_exp/validate.go b/butane/config/fcos/v1_8_exp/validate.go new file mode 100644 index 000000000..a21328e5b --- /dev/null +++ b/butane/config/fcos/v1_8_exp/validate.go @@ -0,0 +1,146 @@ +// 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 v1_8_exp + +import ( + "regexp" + "strings" + + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/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" +) + +const rootDevice = "/dev/disk/by-id/coreos-boot-disk" + +var allowedMountpoints = regexp.MustCompile(`^/(etc|var)(/|$)`) +var dasdRe = regexp.MustCompile("(/dev/dasd[a-z]$)") +var sdRe = regexp.MustCompile("(/dev/sd[a-z]$)") + +// We can't define a Validate function directly on Disk because that's defined in base, +// so we use a Validate function on the top-level Config instead. +func (conf Config) Validate(c path.ContextPath) (r report.Report) { + // Collect mirror device paths so we can skip the reuse-by-label + // check for them; processBootDevice() will set wipe_table: true. + mirrorDevices := make(map[string]bool) + for _, dev := range conf.BootDevice.Mirror.Devices { + mirrorDevices[dev] = true + } + for i, disk := range conf.Storage.Disks { + if disk.Device != rootDevice && !util.IsTrue(disk.WipeTable) && !mirrorDevices[disk.Device] { + for p, partition := range disk.Partitions { + if partition.Number == 0 && partition.Label != nil { + r.AddOnWarn(c.Append("storage", "disks", i, "partitions", p, "number"), common.ErrReuseByLabel) + } + } + } + } + for i, fs := range conf.Storage.Filesystems { + if fs.Path != nil && !allowedMountpoints.MatchString(*fs.Path) && util.IsTrue(fs.WithMountUnit) { + r.AddOnError(c.Append("storage", "filesystems", i, "path"), common.ErrMountPointForbidden) + } + } + return +} + +func (d BootDevice) Validate(c path.ContextPath) (r report.Report) { + if len(d.Mirror.Devices) > 0 && d.Layout == nil { + r.AddOnError(c.Append("mirror"), common.ErrMirrorRequiresLayout) + } + + if d.Layout != nil { + switch *d.Layout { + case "aarch64", "ppc64le", "x86_64": + // Nothing to do + case "s390x-eckd": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !dasdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-zfcp": + if util.NilOrEmpty(d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrNoLuksBootDevice) + } else if !sdRe.MatchString(*d.Luks.Device) { + r.AddOnError(c.Append("layout"), common.ErrLuksBootDeviceBadName) + } + case "s390x-virt": + default: + r.AddOnError(c.Append("layout"), common.ErrUnknownBootDeviceLayout) + } + + // Mirroring the boot disk is not supported on s390x + if strings.HasPrefix(*d.Layout, "s390x") && len(d.Mirror.Devices) > 0 { + r.AddOnError(c.Append("layout"), common.ErrMirrorNotSupport) + } + } + + // CEX is only valid on s390x and incompatible with Clevis + if util.IsTrue(d.Luks.Cex.Enabled) { + if d.Layout == nil { + r.AddOnError(c.Append("luks", "cex"), common.ErrCexArchitectureMismatch) + } else if !strings.HasPrefix(*d.Layout, "s390x") { + r.AddOnError(c.Append("layout"), common.ErrCexArchitectureMismatch) + } + if len(d.Luks.Tang) > 0 || util.IsTrue(d.Luks.Tpm2) { + r.AddOnError(c.Append("luks"), errors.ErrCexWithClevis) + } + } + + r.Merge(d.Mirror.Validate(c.Append("mirror"))) + return +} + +func (l BootDeviceLuks) Validate(c path.ContextPath) (r report.Report) { + if util.NotEmpty(l.Device) { + valid := false + for _, t := range l.Tang { + if t != (base.Tang{}) { + valid = true + } + } + if util.IsTrue(l.Tpm2) { + valid = true + } else if util.IsTrue(l.Cex.Enabled) { + valid = true + } + if !valid { + r.AddOnError(c.Append("luks"), common.ErrNoLuksMethodSpecified) + } + } + return +} + +func (m BootDeviceMirror) Validate(c path.ContextPath) (r report.Report) { + if len(m.Devices) == 1 { + r.AddOnError(c.Append("devices"), common.ErrTooFewMirrorDevices) + } + return +} + +func (user GrubUser) Validate(c path.ContextPath) (r report.Report) { + if user.Name == "" { + r.AddOnError(c.Append("name"), common.ErrGrubUserNameNotSpecified) + } + + if !util.NotEmpty(user.PasswordHash) { + r.AddOnError(c.Append("password_hash"), common.ErrGrubPasswordNotSpecified) + } + return +} diff --git a/butane/config/fcos/v1_8_exp/validate_test.go b/butane/config/fcos/v1_8_exp/validate_test.go new file mode 100644 index 000000000..e9b7f4d00 --- /dev/null +++ b/butane/config/fcos/v1_8_exp/validate_test.go @@ -0,0 +1,638 @@ +// 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 v1_8_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/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" +) + +// 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 + { + `storage: + files: + - path: /z + q: z`, + "unused key q", + 4, + }, + // Butane YAML validation error + { + `storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 5, + }, + // Butane YAML validation warning + { + `storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 4, + }, + // Butane translation error + { + `storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 5, + }, + // Ignition validation error, leaf node + { + `storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 3, + }, + // Ignition validation error, partition + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 6, + }, + // Ignition duplicate key check, paths + { + `storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 4, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + _, r, _ := ToIgn3_7Bytes([]byte(test.in), common.TranslateBytesOptions{}) + assert.Len(t, r.Entries, 1, "unexpected report length") + assert.Equal(t, test.message, r.Entries[0].Message, "bad error") + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil") + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line") + }) + } +} + +// TestValidateBootDevice tests boot device validation +func TestValidateBootDevice(t *testing.T) { + tests := []struct { + in BootDevice + out error + errPath path.ContextPath + }{ + // empty config + { + BootDevice{}, + nil, + path.New("yaml"), + }, + // complete config + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + Threshold: util.IntToPtr(2), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // complete config with cex + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + nil, + path.New("yaml"), + }, + // can not use both cex & tang + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tang: []base.Tang{{ + URL: "https://example.com/", + Thumbprint: util.StrToPtr("x"), + }}, + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use both cex & tpm2 + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + Tpm2: util.BoolToPtr(true), + }, + }, + errors.ErrCexWithClevis, + path.New("yaml", "luks"), + }, + // can not use cex on non s390x + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "layout"), + }, + // must set s390x layout with cex + { + BootDevice{ + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + common.ErrCexArchitectureMismatch, + path.New("yaml", "luks", "cex"), + }, + // invalid layout + { + BootDevice{ + Layout: util.StrToPtr("sparc"), + }, + common.ErrUnknownBootDeviceLayout, + path.New("yaml", "layout"), + }, + // only one mirror device + { + BootDevice{ + Layout: util.StrToPtr("x86_64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda"}, + }, + }, + common.ErrTooFewMirrorDevices, + path.New("yaml", "mirror", "devices"), + }, + // s390x-eckd/s390x-zfcp layouts require a boot device with luks + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + }, + common.ErrNoLuksBootDevice, + path.New("yaml", "layout"), + }, + // s390x-eckd/s390x-zfcp layouts do not support mirroring + { + BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + Mirror: BootDeviceMirror{ + Devices: []string{ + "/dev/sda", + "/dev/sdb", + }, + }, + }, + common.ErrMirrorNotSupport, + path.New("yaml", "layout"), + }, + // s390x-eckd devices must start with /dev/dasd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // s390x-zfcp devices must start with /dev/sd + { + BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasd"), + Tpm2: util.BoolToPtr(true), + }, + }, + common.ErrLuksBootDeviceBadName, + path.New("yaml", "layout"), + }, + // mirror with layout should succeed + { + BootDevice{ + Layout: util.StrToPtr("aarch64"), + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + nil, + path.New("yaml"), + }, + // mirror without layout + { + BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/vda", "/dev/vdb"}, + }, + }, + common.ErrMirrorRequiresLayout, + path.New("yaml", "mirror"), + }, + } + + 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 validation report") + }) + } +} + +func TestValidateGrubUser(t *testing.T) { + tests := []struct { + in GrubUser + out error + errPath path.ContextPath + }{ + // valid user + { + in: GrubUser{ + Name: "name", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: nil, + errPath: path.New("yaml"), + }, + // username is not specified + { + in: GrubUser{ + Name: "", + PasswordHash: util.StrToPtr("pkcs5-pass"), + }, + out: common.ErrGrubUserNameNotSpecified, + errPath: path.New("yaml", "name"), + }, + // password is not specified + { + in: GrubUser{ + Name: "name", + }, + out: common.ErrGrubPasswordNotSpecified, + errPath: path.New("yaml", "password_hash"), + }, + } + + 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 TestValidateMountPoints(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (has prefix "/etc" or "/var") + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/etc/foo"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/var"), + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: util.StrToPtr("/invalid/path"), + WithMountUnit: util.BoolToPtr(false), + }, + { + WithMountUnit: util.BoolToPtr(true), + }, + { + Path: nil, + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + // invalid config (path name is '/') + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /boot) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/boot"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is invalid, does not contain /etc or /var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/thisIsABugTest"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /varnish) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/varnish"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + // invalid config (path is /foo/var) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Path: util.StrToPtr("/foo/var"), + WithMountUnit: util.BoolToPtr(true), + }, + }, + }, + }, + }, + + out: common.ErrMountPointForbidden, + errPath: path.New("yaml", "storage", "filesystems", 0, "path"), + }, + } + 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, "invalid report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // valid config (wipe_table is true) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(true), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is /dev/disk/by-id/coreos-boot-disk) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: rootDevice, + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + }, + }, + }, + // valid config (disk is a boot_device.mirror device) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/sda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("root-1"), + }, + }, + }, + }, + }, + }, + BootDevice: BootDevice{ + Mirror: BootDeviceMirror{ + Devices: []string{"/dev/sda", "/dev/sdb"}, + }, + }, + }, + }, + // invalid config (wipe_table is nil) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + // invalid config (wipe_table is false with a partition numbered 0) + { + in: Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "/dev/vda", + WipeTable: util.BoolToPtr(false), + Partitions: []base.Partition{ + { + Label: util.StrToPtr("foo"), + }, + { + Label: util.StrToPtr("bar"), + Number: 2, + }, + }, + }, + }, + }, + }, + }, + out: common.ErrReuseByLabel, + errPath: path.New("yaml", "storage", "disks", 0, "partitions", 0, "number"), + }, + } + + 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.AddOnWarn(test.errPath, test.out) + assert.Equal(t, expected, actual, "invalid report") + }) + } +} diff --git a/butane/config/fiot/v1_0/schema.go b/butane/config/fiot/v1_0/schema.go new file mode 100644 index 000000000..4aef67df8 --- /dev/null +++ b/butane/config/fiot/v1_0/schema.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_5" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/fiot/v1_0/translate.go b/butane/config/fiot/v1_0/translate.go new file mode 100644 index 000000000..11b61b2bc --- /dev/null +++ b/butane/config/fiot/v1_0/translate.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "kernelArguments": common.ErrGeneralKernelArgumentSupport, + "storage.disks": common.ErrDiskSupport, + "storage.filesystems": common.ErrFilesystemSupport, + "storage.luks": common.ErrLuksSupport, + "storage.raid": common.ErrRaidSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// 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 +} + +// ToIgn3_4Bytes translates from a v1.1 Butane config to a v3.4.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 ToIgn3_4Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) +} diff --git a/butane/config/fiot/v1_0/translate_test.go b/butane/config/fiot/v1_0/translate_test.go new file mode 100644 index 000000000..4f5c2c84d --- /dev/null +++ b/butane/config/fiot/v1_0/translate_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test that we error on unsupported fields for fiot +func TestTranslateInvalid(t *testing.T) { + type InvalidEntry struct { + Kind report.EntryKind + Err error + Path path.ContextPath + } + tests := []struct { + In Config + Entries []InvalidEntry + }{ + // we don't support setting kernel arguments + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // we don't support unsetting kernel arguments either + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldNotExist: []base.KernelArgument{ + "another-test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // disk customizations are made in Image Builder, fiot doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "some-device", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrDiskSupport, + path.New("yaml", "storage", "disks"), + }, + }, + }, + // filesystem customizations are made in Image Builder, fiot doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-label/TEST", + Path: util.StrToPtr("/var"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrFilesystemSupport, + path.New("yaml", "storage", "filesystems"), + }, + }, + }, + // default luks configuration is made in Image Builder for fiot, we don't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Label: util.StrToPtr("some-label"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrLuksSupport, + path.New("yaml", "storage", "luks"), + }, + }, + }, + // we don't support configuring raid via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Raid: []base.Raid{ + { + Name: "some-name", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrRaidSupport, + path.New("yaml", "storage", "raid"), + }, + }, + }, + } + 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.AddOnError(entry.Path, entry.Err) + } + actual, translations, r := test.In.ToIgn3_4Unvalidated(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/butane/config/fiot/v1_1_exp/schema.go b/butane/config/fiot/v1_1_exp/schema.go new file mode 100644 index 000000000..02d800f9b --- /dev/null +++ b/butane/config/fiot/v1_1_exp/schema.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1_exp + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/fiot/v1_1_exp/translate.go b/butane/config/fiot/v1_1_exp/translate.go new file mode 100644 index 000000000..c7b19e3a5 --- /dev/null +++ b/butane/config/fiot/v1_1_exp/translate.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1_exp + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "kernelArguments": common.ErrGeneralKernelArgumentSupport, + "storage.disks": common.ErrDiskSupport, + "storage.filesystems": common.ErrFilesystemSupport, + "storage.luks": common.ErrLuksSupport, + "storage.raid": common.ErrRaidSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToIgn3_7 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_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_7Bytes translates from a v1.2 Butane config to a v3.5.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 ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) +} diff --git a/butane/config/fiot/v1_1_exp/translate_test.go b/butane/config/fiot/v1_1_exp/translate_test.go new file mode 100644 index 000000000..b0d2a1781 --- /dev/null +++ b/butane/config/fiot/v1_1_exp/translate_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test that we error on unsupported fields for fiot +func TestTranslateInvalid(t *testing.T) { + type InvalidEntry struct { + Kind report.EntryKind + Err error + Path path.ContextPath + } + tests := []struct { + In Config + Entries []InvalidEntry + }{ + // we don't support setting kernel arguments + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // we don't support unsetting kernel arguments either + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldNotExist: []base.KernelArgument{ + "another-test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // disk customizations are made in Image Builder, fiot doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "some-device", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrDiskSupport, + path.New("yaml", "storage", "disks"), + }, + }, + }, + // filesystem customizations are made in Image Builder, fiot doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-label/TEST", + Path: util.StrToPtr("/var"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrFilesystemSupport, + path.New("yaml", "storage", "filesystems"), + }, + }, + }, + // default luks configuration is made in Image Builder for fiot, we don't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Label: util.StrToPtr("some-label"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrLuksSupport, + path.New("yaml", "storage", "luks"), + }, + }, + }, + // we don't support configuring raid via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Raid: []base.Raid{ + { + Name: "some-name", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrRaidSupport, + path.New("yaml", "storage", "raid"), + }, + }, + }, + } + 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.AddOnError(entry.Path, entry.Err) + } + actual, translations, r := test.In.ToIgn3_7Unvalidated(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/butane/config/flatcar/v1_0/schema.go b/butane/config/flatcar/v1_0/schema.go new file mode 100644 index 000000000..92d049be7 --- /dev/null +++ b/butane/config/flatcar/v1_0/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_4" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/flatcar/v1_0/translate.go b/butane/config/flatcar/v1_0/translate.go new file mode 100644 index 000000000..d327851cc --- /dev/null +++ b/butane/config/flatcar/v1_0/translate.go @@ -0,0 +1,50 @@ +// 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 v1_0 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "storage.luks.clevis": common.ErrClevisSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToIgn3_3 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_3(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_3Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_3Bytes translates from a v1.0 Butane config to a v3.3.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 ToIgn3_3Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_3", options) +} diff --git a/butane/config/flatcar/v1_0/translate_test.go b/butane/config/flatcar/v1_0/translate_test.go new file mode 100644 index 000000000..c2c926bb5 --- /dev/null +++ b/butane/config/flatcar/v1_0/translate_test.go @@ -0,0 +1,82 @@ +// 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 v1_0 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_4" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test translation of Flatcar support for Ignition config fields. +func TestTranslation(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // all the warnings/errors + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "data", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B"), + }, + { + Name: "data-bis", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B-bis"), + Clevis: base.Clevis{Tpm2: util.BoolToPtr(true)}, + }, + }, + }, + }, + }, + []entry{ + {report.Error, common.ErrClevisSupport, path.New("yaml", "storage", "luks", 1, "clevis")}, + }, + }, + } + + 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.ToIgn3_3Unvalidated(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/butane/config/flatcar/v1_1/schema.go b/butane/config/flatcar/v1_1/schema.go new file mode 100644 index 000000000..13402add3 --- /dev/null +++ b/butane/config/flatcar/v1_1/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_5" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/flatcar/v1_1/translate.go b/butane/config/flatcar/v1_1/translate.go new file mode 100644 index 000000000..d7b972dbb --- /dev/null +++ b/butane/config/flatcar/v1_1/translate.go @@ -0,0 +1,50 @@ +// 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 v1_1 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "storage.luks.clevis": common.ErrClevisSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// 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 +} + +// ToIgn3_4Bytes translates from a v1.1 Butane config to a v3.4.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 ToIgn3_4Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) +} diff --git a/butane/config/flatcar/v1_1/translate_test.go b/butane/config/flatcar/v1_1/translate_test.go new file mode 100644 index 000000000..09e0565c1 --- /dev/null +++ b/butane/config/flatcar/v1_1/translate_test.go @@ -0,0 +1,82 @@ +// 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 v1_1 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test translation of Flatcar support for Ignition config fields. +func TestTranslation(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // all the warnings/errors + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "data", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B"), + }, + { + Name: "data-bis", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B-bis"), + Clevis: base.Clevis{Tpm2: util.BoolToPtr(true)}, + }, + }, + }, + }, + }, + []entry{ + {report.Error, common.ErrClevisSupport, path.New("yaml", "storage", "luks", 1, "clevis")}, + }, + }, + } + + 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.ToIgn3_4Unvalidated(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/butane/config/flatcar/v1_2_exp/schema.go b/butane/config/flatcar/v1_2_exp/schema.go new file mode 100644 index 000000000..cba053952 --- /dev/null +++ b/butane/config/flatcar/v1_2_exp/schema.go @@ -0,0 +1,23 @@ +// Copyright 2020 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2_exp + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/flatcar/v1_2_exp/translate.go b/butane/config/flatcar/v1_2_exp/translate.go new file mode 100644 index 000000000..59ed858fe --- /dev/null +++ b/butane/config/flatcar/v1_2_exp/translate.go @@ -0,0 +1,50 @@ +// 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 v1_2_exp + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "storage.luks.cex": common.ErrCexNotSupported, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToIgn3_5 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_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_5Bytes translates from a v1.2 Butane config to a v3.5.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 ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) +} diff --git a/butane/config/flatcar/v1_2_exp/translate_test.go b/butane/config/flatcar/v1_2_exp/translate_test.go new file mode 100644 index 000000000..d8d0a6abe --- /dev/null +++ b/butane/config/flatcar/v1_2_exp/translate_test.go @@ -0,0 +1,82 @@ +// 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 v1_2_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test translation of Flatcar support for Ignition config fields. +func TestTranslation(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // all the warnings/errors + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "data", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B"), + }, + { + Name: "data-bis", + Device: util.StrToPtr("/dev/disk/by-partlabel/USR-B-bis"), + Clevis: base.Clevis{Tpm2: util.BoolToPtr(true)}, + }, + }, + }, + }, + }, + []entry{}, // Clevis support was added in 1_2 and we therefore expect no errors. + }, + } + + 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.ToIgn3_7Unvalidated(common.TranslateOptions{}) + if test.in.FieldFilters() != nil { + r.Merge(test.in.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/butane/config/openshift/v4_10/result/schema.go b/butane/config/openshift/v4_10/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_10/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_2/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/butane/config/openshift/v4_10/schema.go b/butane/config/openshift/v4_10/schema.go new file mode 100644 index 000000000..50726b3a2 --- /dev/null +++ b/butane/config/openshift/v4_10/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_10 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_10/translate.go b/butane/config/openshift/v4_10/translate.go new file mode 100644 index 000000000..3fd2b6ef9 --- /dev/null +++ b/butane/config/openshift/v4_10/translate.go @@ -0,0 +1,293 @@ +// 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_10 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_10/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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{ + // 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.passwordHash": 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_10Unvalidated 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_10Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_10 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_10(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_10Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_10Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.10 Butane config to a v4.10 MachineConfig or a v3.4.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_10", 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, 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/butane/config/openshift/v4_10/translate_test.go b/butane/config/openshift/v4_10/translate_test.go new file mode 100644 index 000000000..5b5483c36 --- /dev/null +++ b/butane/config/openshift/v4_10/translate_test.go @@ -0,0 +1,500 @@ +// 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_10 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_10/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(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.2.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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_10Unvalidated(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", + 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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "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.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, "password_hash")}, + {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_10Unvalidated(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/butane/config/openshift/v4_10/validate.go b/butane/config/openshift/v4_10/validate.go new file mode 100644 index 000000000..48a26232f --- /dev/null +++ b/butane/config/openshift/v4_10/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_10 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_10/validate_test.go b/butane/config/openshift/v4_10/validate_test.go new file mode 100644 index 000000000..cfe27dce3 --- /dev/null +++ b/butane/config/openshift/v4_10/validate_test.go @@ -0,0 +1,235 @@ +// 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_10 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/openshift/v4_11/result/schema.go b/butane/config/openshift/v4_11/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_11/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_2/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/butane/config/openshift/v4_11/schema.go b/butane/config/openshift/v4_11/schema.go new file mode 100644 index 000000000..ead5ffb37 --- /dev/null +++ b/butane/config/openshift/v4_11/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_11 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_11/translate.go b/butane/config/openshift/v4_11/translate.go new file mode 100644 index 000000000..66fe9aa01 --- /dev/null +++ b/butane/config/openshift/v4_11/translate.go @@ -0,0 +1,293 @@ +// 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_11 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_11/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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{ + // 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.passwordHash": 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_11Unvalidated 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_11Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_11 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_11(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_11Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_11Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.11 Butane config to a v4.11 MachineConfig or a v3.4.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_11", 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, 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/butane/config/openshift/v4_11/translate_test.go b/butane/config/openshift/v4_11/translate_test.go new file mode 100644 index 000000000..cc80470cf --- /dev/null +++ b/butane/config/openshift/v4_11/translate_test.go @@ -0,0 +1,500 @@ +// 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_11 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_11/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(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.2.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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_11Unvalidated(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", + 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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "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.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, "password_hash")}, + {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_11Unvalidated(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/butane/config/openshift/v4_11/validate.go b/butane/config/openshift/v4_11/validate.go new file mode 100644 index 000000000..65e2030f0 --- /dev/null +++ b/butane/config/openshift/v4_11/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_11 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_11/validate_test.go b/butane/config/openshift/v4_11/validate_test.go new file mode 100644 index 000000000..6c8dca9a4 --- /dev/null +++ b/butane/config/openshift/v4_11/validate_test.go @@ -0,0 +1,235 @@ +// 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_11 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/openshift/v4_12/result/schema.go b/butane/config/openshift/v4_12/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_12/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_2/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/butane/config/openshift/v4_12/schema.go b/butane/config/openshift/v4_12/schema.go new file mode 100644 index 000000000..2ff1769a2 --- /dev/null +++ b/butane/config/openshift/v4_12/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_12 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_12/translate.go b/butane/config/openshift/v4_12/translate.go new file mode 100644 index 000000000..133f94bc7 --- /dev/null +++ b/butane/config/openshift/v4_12/translate.go @@ -0,0 +1,293 @@ +// 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_12 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_12/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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{ + // 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.passwordHash": 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_12Unvalidated 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_12Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_12 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_12(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_12Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_12Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.12 Butane config to a v4.12 MachineConfig or a v3.2.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_12", 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, 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/butane/config/openshift/v4_12/translate_test.go b/butane/config/openshift/v4_12/translate_test.go new file mode 100644 index 000000000..55558297c --- /dev/null +++ b/butane/config/openshift/v4_12/translate_test.go @@ -0,0 +1,500 @@ +// 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_12 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_12/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(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.2.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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_12Unvalidated(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", + 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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "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.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, "password_hash")}, + {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_12Unvalidated(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/butane/config/openshift/v4_12/validate.go b/butane/config/openshift/v4_12/validate.go new file mode 100644 index 000000000..bae4bcaeb --- /dev/null +++ b/butane/config/openshift/v4_12/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_12 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_12/validate_test.go b/butane/config/openshift/v4_12/validate_test.go new file mode 100644 index 000000000..998e785f8 --- /dev/null +++ b/butane/config/openshift/v4_12/validate_test.go @@ -0,0 +1,235 @@ +// 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_12 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/openshift/v4_13/result/schema.go b/butane/config/openshift/v4_13/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_13/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_2/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/butane/config/openshift/v4_13/schema.go b/butane/config/openshift/v4_13/schema.go new file mode 100644 index 000000000..6c6857b76 --- /dev/null +++ b/butane/config/openshift/v4_13/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_13 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_13/translate.go b/butane/config/openshift/v4_13/translate.go new file mode 100644 index 000000000..bb5baf5e1 --- /dev/null +++ b/butane/config/openshift/v4_13/translate.go @@ -0,0 +1,291 @@ +// 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_13 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_13/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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{ + // 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_13Unvalidated 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_13Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_13 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_13(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_13Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_13Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.13 Butane config to a v4.13 MachineConfig or a v3.2.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_13", 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, 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/butane/config/openshift/v4_13/translate_test.go b/butane/config/openshift/v4_13/translate_test.go new file mode 100644 index 000000000..f0693bcfd --- /dev/null +++ b/butane/config/openshift/v4_13/translate_test.go @@ -0,0 +1,500 @@ +// 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_13 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_13/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(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.2.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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_13Unvalidated(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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "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.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_13Unvalidated(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/butane/config/openshift/v4_13/validate.go b/butane/config/openshift/v4_13/validate.go new file mode 100644 index 000000000..6609960c2 --- /dev/null +++ b/butane/config/openshift/v4_13/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_13 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_13/validate_test.go b/butane/config/openshift/v4_13/validate_test.go new file mode 100644 index 000000000..6fc27dcec --- /dev/null +++ b/butane/config/openshift/v4_13/validate_test.go @@ -0,0 +1,235 @@ +// 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_13 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/openshift/v4_14/result/schema.go b/butane/config/openshift/v4_14/result/schema.go new file mode 100644 index 000000000..0a00d604a --- /dev/null +++ b/butane/config/openshift/v4_14/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/butane/config/openshift/v4_14/schema.go b/butane/config/openshift/v4_14/schema.go new file mode 100644 index 000000000..c4da069db --- /dev/null +++ b/butane/config/openshift/v4_14/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_14 + +import ( + fcos "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_14/translate.go b/butane/config/openshift/v4_14/translate.go new file mode 100644 index 000000000..3a6cc9a97 --- /dev/null +++ b/butane/config/openshift/v4_14/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_14 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_14/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_14Unvalidated 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_14Unvalidated(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_14 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_14(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_14Unvalidated", 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_14Unvalidated(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.14 Butane config to a v4.14 MachineConfig or a v3.4.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_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_14", 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/butane/config/openshift/v4_14/translate_test.go b/butane/config/openshift/v4_14/translate_test.go new file mode 100644 index 000000000..9afdc7c0f --- /dev/null +++ b/butane/config/openshift/v4_14/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_14 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_14/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_14Unvalidated(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_14Unvalidated(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/butane/config/openshift/v4_14/validate.go b/butane/config/openshift/v4_14/validate.go new file mode 100644 index 000000000..a20915c1d --- /dev/null +++ b/butane/config/openshift/v4_14/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_14 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_14/validate_test.go b/butane/config/openshift/v4_14/validate_test.go new file mode 100644 index 000000000..ec7ac4e1b --- /dev/null +++ b/butane/config/openshift/v4_14/validate_test.go @@ -0,0 +1,236 @@ +// 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_14 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 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/butane/config/openshift/v4_15/result/schema.go b/butane/config/openshift/v4_15/result/schema.go new file mode 100644 index 000000000..0a00d604a --- /dev/null +++ b/butane/config/openshift/v4_15/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/butane/config/openshift/v4_15/schema.go b/butane/config/openshift/v4_15/schema.go new file mode 100644 index 000000000..8f2834ff2 --- /dev/null +++ b/butane/config/openshift/v4_15/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_15 + +import ( + fcos "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_15/translate.go b/butane/config/openshift/v4_15/translate.go new file mode 100644 index 000000000..b54c19a73 --- /dev/null +++ b/butane/config/openshift/v4_15/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_15 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_15/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_15Unvalidated 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_15Unvalidated(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_15 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_15(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_15Unvalidated", 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_15Unvalidated(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.15 Butane config to a v4.15 MachineConfig or a v3.5.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_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_15", 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/butane/config/openshift/v4_15/translate_test.go b/butane/config/openshift/v4_15/translate_test.go new file mode 100644 index 000000000..9db7e511a --- /dev/null +++ b/butane/config/openshift/v4_15/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_15 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_15/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_15Unvalidated(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_15Unvalidated(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/butane/config/openshift/v4_15/validate.go b/butane/config/openshift/v4_15/validate.go new file mode 100644 index 000000000..9240a2997 --- /dev/null +++ b/butane/config/openshift/v4_15/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_15 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_15/validate_test.go b/butane/config/openshift/v4_15/validate_test.go new file mode 100644 index 000000000..72a4824cc --- /dev/null +++ b/butane/config/openshift/v4_15/validate_test.go @@ -0,0 +1,236 @@ +// 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_15 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 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/butane/config/openshift/v4_16/result/schema.go b/butane/config/openshift/v4_16/result/schema.go new file mode 100644 index 000000000..0a00d604a --- /dev/null +++ b/butane/config/openshift/v4_16/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/butane/config/openshift/v4_16/schema.go b/butane/config/openshift/v4_16/schema.go new file mode 100644 index 000000000..c7f850163 --- /dev/null +++ b/butane/config/openshift/v4_16/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_16 + +import ( + fcos "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_16/translate.go b/butane/config/openshift/v4_16/translate.go new file mode 100644 index 000000000..143240dbc --- /dev/null +++ b/butane/config/openshift/v4_16/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_16 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_16/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_16Unvalidated 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_16Unvalidated(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_16 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_16(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_16Unvalidated", 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_16Unvalidated(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.16 Butane config to a v4.16 MachineConfig or a v3.4.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_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_16", 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/butane/config/openshift/v4_16/translate_test.go b/butane/config/openshift/v4_16/translate_test.go new file mode 100644 index 000000000..b74bb045f --- /dev/null +++ b/butane/config/openshift/v4_16/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_16 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_16/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_16Unvalidated(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_16Unvalidated(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/butane/config/openshift/v4_16/validate.go b/butane/config/openshift/v4_16/validate.go new file mode 100644 index 000000000..3a400d26c --- /dev/null +++ b/butane/config/openshift/v4_16/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_16 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_16/validate_test.go b/butane/config/openshift/v4_16/validate_test.go new file mode 100644 index 000000000..b6a909ef1 --- /dev/null +++ b/butane/config/openshift/v4_16/validate_test.go @@ -0,0 +1,236 @@ +// 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_16 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 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/butane/config/openshift/v4_17/result/schema.go b/butane/config/openshift/v4_17/result/schema.go new file mode 100644 index 000000000..0a00d604a --- /dev/null +++ b/butane/config/openshift/v4_17/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/butane/config/openshift/v4_17/schema.go b/butane/config/openshift/v4_17/schema.go new file mode 100644 index 000000000..3cbab4c1a --- /dev/null +++ b/butane/config/openshift/v4_17/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_17 + +import ( + fcos "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_17/translate.go b/butane/config/openshift/v4_17/translate.go new file mode 100644 index 000000000..a13abc595 --- /dev/null +++ b/butane/config/openshift/v4_17/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_17 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_17/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_17Unvalidated 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_17Unvalidated(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_17 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_17(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_17Unvalidated", 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_17Unvalidated(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.5.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_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_17", 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/butane/config/openshift/v4_17/translate_test.go b/butane/config/openshift/v4_17/translate_test.go new file mode 100644 index 000000000..0fb8a1171 --- /dev/null +++ b/butane/config/openshift/v4_17/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_17 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_17/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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_17Unvalidated(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_17Unvalidated(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/butane/config/openshift/v4_17/validate.go b/butane/config/openshift/v4_17/validate.go new file mode 100644 index 000000000..32204c94d --- /dev/null +++ b/butane/config/openshift/v4_17/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_17 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_17/validate_test.go b/butane/config/openshift/v4_17/validate_test.go new file mode 100644 index 000000000..9e4aec73f --- /dev/null +++ b/butane/config/openshift/v4_17/validate_test.go @@ -0,0 +1,236 @@ +// 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_17 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 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/butane/config/openshift/v4_18/result/schema.go b/butane/config/openshift/v4_18/result/schema.go new file mode 100644 index 000000000..0a00d604a --- /dev/null +++ b/butane/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/butane/config/openshift/v4_18/schema.go b/butane/config/openshift/v4_18/schema.go new file mode 100644 index 000000000..72abc4a3c --- /dev/null +++ b/butane/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/ignition/v2/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/butane/config/openshift/v4_18/translate.go b/butane/config/openshift/v4_18/translate.go new file mode 100644 index 000000000..2ca9cc89b --- /dev/null +++ b/butane/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/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_18/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_18/translate_test.go b/butane/config/openshift/v4_18/translate_test.go new file mode 100644 index 000000000..8295ea2d2 --- /dev/null +++ b/butane/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/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_5" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_18/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_18/validate.go b/butane/config/openshift/v4_18/validate.go new file mode 100644 index 000000000..856b975b3 --- /dev/null +++ b/butane/config/openshift/v4_18/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_18 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_18/validate_test.go b/butane/config/openshift/v4_18/validate_test.go new file mode 100644 index 000000000..18e8240a5 --- /dev/null +++ b/butane/config/openshift/v4_18/validate_test.go @@ -0,0 +1,236 @@ +// 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/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 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/butane/config/openshift/v4_19/result/schema.go b/butane/config/openshift/v4_19/result/schema.go new file mode 100644 index 000000000..afaf958df --- /dev/null +++ b/butane/config/openshift/v4_19/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_5/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/butane/config/openshift/v4_19/schema.go b/butane/config/openshift/v4_19/schema.go new file mode 100644 index 000000000..c9d6d629e --- /dev/null +++ b/butane/config/openshift/v4_19/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_19 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" +) + +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/butane/config/openshift/v4_19/translate.go b/butane/config/openshift/v4_19/translate.go new file mode 100644 index 000000000..71cbaef9f --- /dev/null +++ b/butane/config/openshift/v4_19/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_19 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_19/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/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_19Unvalidated 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_19Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_5Unvalidated(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_19 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_19(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_19Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_5Unvalidated 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_5Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_19Unvalidated(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_5 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_5(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_5Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.19 Butane config to a v4.19 MachineConfig or a v3.5.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_5", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_19", 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/butane/config/openshift/v4_19/translate_test.go b/butane/config/openshift/v4_19/translate_test.go new file mode 100644 index 000000000..4acbfce85 --- /dev/null +++ b/butane/config/openshift/v4_19/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_19 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_19/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/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_5Unvalidated(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.5.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.5.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_19Unvalidated(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_19Unvalidated(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/butane/config/openshift/v4_19/validate.go b/butane/config/openshift/v4_19/validate.go new file mode 100644 index 000000000..414ea17ba --- /dev/null +++ b/butane/config/openshift/v4_19/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_19 + +import ( + "slices" + + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_19/validate_test.go b/butane/config/openshift/v4_19/validate_test.go new file mode 100644 index 000000000..95247a629 --- /dev/null +++ b/butane/config/openshift/v4_19/validate_test.go @@ -0,0 +1,370 @@ +// 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 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + + "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 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/butane/config/openshift/v4_20/result/schema.go b/butane/config/openshift/v4_20/result/schema.go new file mode 100644 index 000000000..afaf958df --- /dev/null +++ b/butane/config/openshift/v4_20/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_5/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/butane/config/openshift/v4_20/schema.go b/butane/config/openshift/v4_20/schema.go new file mode 100644 index 000000000..d07343bc8 --- /dev/null +++ b/butane/config/openshift/v4_20/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_20 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" +) + +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/butane/config/openshift/v4_20/translate.go b/butane/config/openshift/v4_20/translate.go new file mode 100644 index 000000000..ae439a727 --- /dev/null +++ b/butane/config/openshift/v4_20/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_20 + +import ( + "net/url" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_20/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_5/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_20Unvalidated 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_20Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_5Unvalidated(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_20 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_20(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_20Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_5Unvalidated 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_5Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_20Unvalidated(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_5 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_5(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_5Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.20 Butane config to a v4.20 MachineConfig or a v3.5.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_5", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_20", 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/butane/config/openshift/v4_20/translate_test.go b/butane/config/openshift/v4_20/translate_test.go new file mode 100644 index 000000000..a940660c1 --- /dev/null +++ b/butane/config/openshift/v4_20/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_20 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_20/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/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_5Unvalidated(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.5.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_20Unvalidated(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_20Unvalidated(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/butane/config/openshift/v4_20/validate.go b/butane/config/openshift/v4_20/validate.go new file mode 100644 index 000000000..6deab49c4 --- /dev/null +++ b/butane/config/openshift/v4_20/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_20 + +import ( + "slices" + + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_20/validate_test.go b/butane/config/openshift/v4_20/validate_test.go new file mode 100644 index 000000000..a83ac5941 --- /dev/null +++ b/butane/config/openshift/v4_20/validate_test.go @@ -0,0 +1,370 @@ +// 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_20 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + + "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 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/butane/config/openshift/v4_21/result/schema.go b/butane/config/openshift/v4_21/result/schema.go new file mode 100644 index 000000000..afaf958df --- /dev/null +++ b/butane/config/openshift/v4_21/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_5/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/butane/config/openshift/v4_21/schema.go b/butane/config/openshift/v4_21/schema.go new file mode 100644 index 000000000..5fe5012f7 --- /dev/null +++ b/butane/config/openshift/v4_21/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_21 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" +) + +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/butane/config/openshift/v4_21/translate.go b/butane/config/openshift/v4_21/translate.go new file mode 100644 index 000000000..af9accbd4 --- /dev/null +++ b/butane/config/openshift/v4_21/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_21 + +import ( + "net/url" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_21/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_5/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_21Unvalidated 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_21Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_5Unvalidated(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_21 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_21(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_21Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_5Unvalidated 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_5Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_21Unvalidated(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_5 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_5(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_5Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.21 Butane config to a v4.21 MachineConfig or a v3.5.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_5", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_21", 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/butane/config/openshift/v4_21/translate_test.go b/butane/config/openshift/v4_21/translate_test.go new file mode 100644 index 000000000..07e722444 --- /dev/null +++ b/butane/config/openshift/v4_21/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_21 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_21/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/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_5Unvalidated(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.5.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_21Unvalidated(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_21Unvalidated(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/butane/config/openshift/v4_21/validate.go b/butane/config/openshift/v4_21/validate.go new file mode 100644 index 000000000..edfa4eb34 --- /dev/null +++ b/butane/config/openshift/v4_21/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_21 + +import ( + "slices" + + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_21/validate_test.go b/butane/config/openshift/v4_21/validate_test.go new file mode 100644 index 000000000..3d1ba2d46 --- /dev/null +++ b/butane/config/openshift/v4_21/validate_test.go @@ -0,0 +1,370 @@ +// 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_21 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + + "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 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/butane/config/openshift/v4_22/result/schema.go b/butane/config/openshift/v4_22/result/schema.go new file mode 100644 index 000000000..afaf958df --- /dev/null +++ b/butane/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_5/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/butane/config/openshift/v4_22/schema.go b/butane/config/openshift/v4_22/schema.go new file mode 100644 index 000000000..e3ed1b6b8 --- /dev/null +++ b/butane/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/ignition/v2/butane/config/fcos/v1_6" +) + +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/butane/config/openshift/v4_22/translate.go b/butane/config/openshift/v4_22/translate.go new file mode 100644 index 000000000..63b3425c1 --- /dev/null +++ b/butane/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/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_22/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_5/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_5Unvalidated(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_5Unvalidated 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_5Unvalidated(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_5 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_5(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_5Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.22 Butane config to a v4.22 MachineConfig or a v3.5.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_5", 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/butane/config/openshift/v4_22/translate_test.go b/butane/config/openshift/v4_22/translate_test.go new file mode 100644 index 000000000..ba9abd46e --- /dev/null +++ b/butane/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/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_22/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_5/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_5Unvalidated(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.5.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/butane/config/openshift/v4_22/validate.go b/butane/config/openshift/v4_22/validate.go new file mode 100644 index 000000000..d4f2dac6e --- /dev/null +++ b/butane/config/openshift/v4_22/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_22 + +import ( + "slices" + + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_22/validate_test.go b/butane/config/openshift/v4_22/validate_test.go new file mode 100644 index 000000000..1e57eb72a --- /dev/null +++ b/butane/config/openshift/v4_22/validate_test.go @@ -0,0 +1,370 @@ +// 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/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_6" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_6" + + "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 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/butane/config/openshift/v4_23_exp/result/schema.go b/butane/config/openshift/v4_23_exp/result/schema.go new file mode 100644 index 000000000..bdf1ad206 --- /dev/null +++ b/butane/config/openshift/v4_23_exp/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_7_experimental/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/butane/config/openshift/v4_23_exp/schema.go b/butane/config/openshift/v4_23_exp/schema.go new file mode 100644 index 000000000..bc2f2135e --- /dev/null +++ b/butane/config/openshift/v4_23_exp/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_23_exp + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_8_exp" +) + +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/butane/config/openshift/v4_23_exp/translate.go b/butane/config/openshift/v4_23_exp/translate.go new file mode 100644 index 000000000..a64f86b62 --- /dev/null +++ b/butane/config/openshift/v4_23_exp/translate.go @@ -0,0 +1,285 @@ +// 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_23_exp + +import ( + "net/url" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_23_exp/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_7_experimental/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_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_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 + } + ts = translateUserGrubCfg(&cfg, &ts) + + // 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_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_23(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_23Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_7Unvalidated 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_7Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_23Unvalidated(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_7 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_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// 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_23", 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 +} + +// fcos config generates a user.cfg file using append; however, OpenShift config +// does not support append (since MCO does not support it). Let change the file to use contents +func translateUserGrubCfg(config *types.Config, ts *translate.TranslationSet) translate.TranslationSet { + newMappings := translate.NewTranslationSet("json", "json") + for i, file := range config.Storage.Files { + if file.Path == "/boot/grub2/user.cfg" { + if len(file.Append) != 1 { + // The number of append objects was different from expected, this file + // was created by the user and not via butane GRUB sugar + return *ts + } + fromPath := path.New("json", "storage", "files", i, "append", 0) + translatedPath := path.New("json", "storage", "files", i, "contents") + config.Storage.Files[i].Contents = file.Append[0] + config.Storage.Files[i].Append = nil + newMappings.AddFromCommonObject(fromPath, translatedPath, config.Storage.Files[i].Contents) + + return ts.Map(newMappings) + } + } + return *ts +} diff --git a/butane/config/openshift/v4_23_exp/translate_test.go b/butane/config/openshift/v4_23_exp/translate_test.go new file mode 100644 index 000000000..13980a3e2 --- /dev/null +++ b/butane/config/openshift/v4_23_exp/translate_test.go @@ -0,0 +1,424 @@ +// 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 ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_8_exp" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_23_exp/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_7_experimental/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_7Unvalidated(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.7.0-experimental", + }, + }, + }, + }, + []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")}, + }, + }, + // Test Grub config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Grub: fcos.Grub{ + Users: []fcos.GrubUser{ + { + Name: "root", + PasswordHash: util.StrToPtr("grub.pbkdf2.sha512.10000.874A958E526409..."), + }, + }, + }, + }, + }, + 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.7.0-experimental", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/disk/by-label/boot", + Format: util.StrToPtr("ext4"), + Path: util.StrToPtr("/boot"), + }, + }, + Files: []types.File{ + { + Node: types.Node{ + Path: "/boot/grub2/user.cfg", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,%23%20Generated%20by%20Butane%0A%0Aset%20superusers%3D%22root%22%0Apassword_pbkdf2%20root%20grub.pbkdf2.sha512.10000.874A958E526409...%0A"), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + }, + }, + []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", "grub", "users"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "path")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0)}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0, "path")}, + // "append" field is a remnant of translations performed in fcos config + // TODO: add a delete function to translation.TranslationSet and delete "append" translation + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0, "append")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "source")}, + {From: path.New("yaml", "grub", "users"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "compression")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + 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") + 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_23Unvalidated(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/butane/config/openshift/v4_23_exp/validate.go b/butane/config/openshift/v4_23_exp/validate.go new file mode 100644 index 000000000..a0e01958c --- /dev/null +++ b/butane/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/ignition/v2/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/butane/config/openshift/v4_23_exp/validate_test.go b/butane/config/openshift/v4_23_exp/validate_test.go new file mode 100644 index 000000000..a51e351b9 --- /dev/null +++ b/butane/config/openshift/v4_23_exp/validate_test.go @@ -0,0 +1,370 @@ +// 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 ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_8_exp" + + "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 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/butane/config/openshift/v4_8/result/schema.go b/butane/config/openshift/v4_8/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_8/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_2/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/butane/config/openshift/v4_8/schema.go b/butane/config/openshift/v4_8/schema.go new file mode 100644 index 000000000..ba71c751b --- /dev/null +++ b/butane/config/openshift/v4_8/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_8 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_8/translate.go b/butane/config/openshift/v4_8/translate.go new file mode 100644 index 000000000..c21e31ceb --- /dev/null +++ b/butane/config/openshift/v4_8/translate.go @@ -0,0 +1,302 @@ +// 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_8 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_8/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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. +// +// BUGGED - Ignored by the MCD but not by Ignition. Ignition correctly +// applies the setting, but the MCD doesn't, and writes incorrect state to +// the node. + +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.NewFiltersIgnoreZero(result.MachineConfig{}, cutil.FilterMap{ + // 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.passwordHash": 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, + // BUGGED + // https://bugzilla.redhat.com/show_bug.cgi?id=1970218 + // We ignoreZero because desugaring inline/local can + // produce StrToPtr("") which is harmless. + "spec.config.storage.files.contents.compression": common.ErrFileCompressionSupport, + // 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, + }, []string{"spec.config.storage.files.contents.compression"}) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_8Unvalidated 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_8Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + // disable inline resource compression since the MCO doesn't support it + // https://bugzilla.redhat.com/show_bug.cgi?id=1970218 + options.NoResourceAutoCompression = true + + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_8 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_8(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_8Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_8Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.8 Butane config to a v4.8 MachineConfig or a v3.2.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_8", 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, 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) + } + } + } + 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/butane/config/openshift/v4_8/translate_test.go b/butane/config/openshift/v4_8/translate_test.go new file mode 100644 index 000000000..ef4b70c11 --- /dev/null +++ b/butane/config/openshift/v4_8/translate_test.go @@ -0,0 +1,603 @@ +// 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_8 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_8/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + 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.2.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")}, + }, + }, + // ensure automatic compression is disabled + { + 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: "/z", + Contents: base.Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + }, + }, + }, + }, + }, + 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.2.0", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/z", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + }, + }, + []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", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "storage", "files"), To: path.New("json", "spec", "config", "storage", "files")}, + {From: path.New("yaml", "storage", "files", 0), To: path.New("json", "spec", "config", "storage", "files", 0)}, + {From: path.New("yaml", "storage", "files", 0, "path"), To: path.New("json", "spec", "config", "storage", "files", 0, "path")}, + {From: path.New("yaml", "storage", "files", 0, "contents"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents")}, + {From: path.New("yaml", "storage", "files", 0, "contents", "inline"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "source")}, + {From: path.New("yaml", "storage", "files", 0, "contents", "inline"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "compression")}, + }, + }, + // 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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_8Unvalidated(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", + 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{}, + }, + // config with uncompressed inline contents + // (shouldn't reject Compression: StrToPtr("")) + { + 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: "/a", + Contents: base.Resource{ + Inline: util.StrToPtr("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"), + }, + }, + Contents: base.Resource{ + Inline: util.StrToPtr("z"), + Compression: util.StrToPtr("gzip"), + }, + }, + { + Path: "/h", + Contents: base.Resource{ + Source: util.StrToPtr("https://example.com/"), + }, + }, + { + 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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "format")}, + {report.Error, common.ErrFileSchemeSupport, path.New("yaml", "storage", "files", 2, "contents", "source")}, + {report.Error, common.ErrUserNameSupport, path.New("yaml", "passwd", "users", 1, "name")}, + // filters + {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, "password_hash")}, + {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.ErrFileCompressionSupport, path.New("yaml", "storage", "files", 1, "contents", "compression")}, + {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_8Unvalidated(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/butane/config/openshift/v4_8/validate.go b/butane/config/openshift/v4_8/validate.go new file mode 100644 index 000000000..e39e7e729 --- /dev/null +++ b/butane/config/openshift/v4_8/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_8 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_8/validate_test.go b/butane/config/openshift/v4_8/validate_test.go new file mode 100644 index 000000000..3f2cf83f9 --- /dev/null +++ b/butane/config/openshift/v4_8/validate_test.go @@ -0,0 +1,235 @@ +// 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_8 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/openshift/v4_9/result/schema.go b/butane/config/openshift/v4_9/result/schema.go new file mode 100644 index 000000000..aee5cd547 --- /dev/null +++ b/butane/config/openshift/v4_9/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_2/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/butane/config/openshift/v4_9/schema.go b/butane/config/openshift/v4_9/schema.go new file mode 100644 index 000000000..1f242e5a7 --- /dev/null +++ b/butane/config/openshift/v4_9/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_9 + +import ( + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" +) + +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/butane/config/openshift/v4_9/translate.go b/butane/config/openshift/v4_9/translate.go new file mode 100644 index 000000000..61e6980f4 --- /dev/null +++ b/butane/config/openshift/v4_9/translate.go @@ -0,0 +1,302 @@ +// 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_9 + +import ( + "net/url" + "strings" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_9/result" + cutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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. +// +// 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. +// +// BUGGED - Ignored by the MCD but not by Ignition. Ignition correctly +// applies the setting, but the MCD doesn't, and writes incorrect state to +// the node. + +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.NewFiltersIgnoreZero(result.MachineConfig{}, cutil.FilterMap{ + // 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.passwordHash": 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, + // BUGGED + // https://bugzilla.redhat.com/show_bug.cgi?id=1970218 + // We ignoreZero because desugaring inline/local can + // produce StrToPtr("") which is harmless. + "spec.config.storage.files.contents.compression": common.ErrFileCompressionSupport, + // 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, + }, []string{"spec.config.storage.files.contents.compression"}) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_9Unvalidated 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_9Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + // disable inline resource compression since the MCO doesn't support it + // https://bugzilla.redhat.com/show_bug.cgi?id=1970218 + options.NoResourceAutoCompression = true + + cfg, ts, r := c.Config.ToIgn3_2Unvalidated(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_9 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_9(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_9Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_2Unvalidated 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_2Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_9Unvalidated(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_2 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_2(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_2Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.9 Butane config to a v4.9 MachineConfig or a v3.2.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_2", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_9", 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, 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) + } + } + } + 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/butane/config/openshift/v4_9/translate_test.go b/butane/config/openshift/v4_9/translate_test.go new file mode 100644 index 000000000..708dfd35d --- /dev/null +++ b/butane/config/openshift/v4_9/translate_test.go @@ -0,0 +1,603 @@ +// 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_9 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_3" + "github.com/coreos/ignition/v2/butane/config/common" + fcos "github.com/coreos/ignition/v2/butane/config/fcos/v1_3" + "github.com/coreos/ignition/v2/butane/config/openshift/v4_9/result" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_2/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_2Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + zzz := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + 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.2.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")}, + }, + }, + // ensure automatic compression is disabled + { + 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: "/z", + Contents: base.Resource{ + Inline: util.StrToPtr(zzz), + }, + }, + }, + }, + }, + }, + }, + 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.2.0", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/z", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:," + zzz), + Compression: util.StrToPtr(""), + }, + }, + }, + }, + }, + }, + }, + }, + []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", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "storage", "files"), To: path.New("json", "spec", "config", "storage", "files")}, + {From: path.New("yaml", "storage", "files", 0), To: path.New("json", "spec", "config", "storage", "files", 0)}, + {From: path.New("yaml", "storage", "files", 0, "path"), To: path.New("json", "spec", "config", "storage", "files", 0, "path")}, + {From: path.New("yaml", "storage", "files", 0, "contents"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents")}, + {From: path.New("yaml", "storage", "files", 0, "contents", "inline"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "source")}, + {From: path.New("yaml", "storage", "files", 0, "contents", "inline"), To: path.New("json", "spec", "config", "storage", "files", 0, "contents", "compression")}, + }, + }, + // 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: []base.LuksOption{"b", "b"}, + }, + { + Name: "c", + Options: []base.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []base.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []base.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []base.LuksOption{"--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.2.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_9Unvalidated(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", + 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{}, + }, + // config with uncompressed inline contents + // (shouldn't reject Compression: StrToPtr("")) + { + 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: "/a", + Contents: base.Resource{ + Inline: util.StrToPtr("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"), + }, + }, + Contents: base.Resource{ + Inline: util.StrToPtr("z"), + Compression: util.StrToPtr("gzip"), + }, + }, + { + Path: "/h", + Contents: base.Resource{ + Source: util.StrToPtr("https://example.com/"), + }, + }, + { + 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"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: "/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"}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "format")}, + {report.Error, common.ErrFileSchemeSupport, path.New("yaml", "storage", "files", 2, "contents", "source")}, + {report.Error, common.ErrUserNameSupport, path.New("yaml", "passwd", "users", 1, "name")}, + // filters + {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, "password_hash")}, + {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.ErrFileCompressionSupport, path.New("yaml", "storage", "files", 1, "contents", "compression")}, + {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_9Unvalidated(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/butane/config/openshift/v4_9/validate.go b/butane/config/openshift/v4_9/validate.go new file mode 100644 index 000000000..2d67c380d --- /dev/null +++ b/butane/config/openshift/v4_9/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_9 + +import ( + "github.com/coreos/ignition/v2/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/butane/config/openshift/v4_9/validate_test.go b/butane/config/openshift/v4_9/validate_test.go new file mode 100644 index 000000000..448b85884 --- /dev/null +++ b/butane/config/openshift/v4_9/validate_test.go @@ -0,0 +1,235 @@ +// 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_9 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + "github.com/coreos/ignition/v2/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 + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // 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/butane/config/r4e/v1_0/schema.go b/butane/config/r4e/v1_0/schema.go new file mode 100644 index 000000000..55532cf0b --- /dev/null +++ b/butane/config/r4e/v1_0/schema.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_4" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/r4e/v1_0/translate.go b/butane/config/r4e/v1_0/translate.go new file mode 100644 index 000000000..157faef5f --- /dev/null +++ b/butane/config/r4e/v1_0/translate.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_3/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "kernelArguments": common.ErrGeneralKernelArgumentSupport, + "storage.disks": common.ErrDiskSupport, + "storage.filesystems": common.ErrFilesystemSupport, + "storage.luks": common.ErrLuksSupport, + "storage.raid": common.ErrRaidSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToIgn3_3 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_3(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_3Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_3Bytes translates from a v1.0 Butane config to a v3.3.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 ToIgn3_3Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_3", options) +} diff --git a/butane/config/r4e/v1_0/translate_test.go b/butane/config/r4e/v1_0/translate_test.go new file mode 100644 index 000000000..b6c4c1ab1 --- /dev/null +++ b/butane/config/r4e/v1_0/translate_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_0 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_4" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test that we error on unsupported fields for r4e +func TestTranslateInvalid(t *testing.T) { + type InvalidEntry struct { + Kind report.EntryKind + Err error + Path path.ContextPath + } + tests := []struct { + In Config + Entries []InvalidEntry + }{ + // we don't support setting kernel arguments + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // we don't support unsetting kernel arguments either + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldNotExist: []base.KernelArgument{ + "another-test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // disk customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "some-device", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrDiskSupport, + path.New("yaml", "storage", "disks"), + }, + }, + }, + // filesystem customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-label/TEST", + Path: util.StrToPtr("/var"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrFilesystemSupport, + path.New("yaml", "storage", "filesystems"), + }, + }, + }, + // default luks configuration is made in Image Builder for r4e, we don't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Label: util.StrToPtr("some-label"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrLuksSupport, + path.New("yaml", "storage", "luks"), + }, + }, + }, + // we don't support configuring raid via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Raid: []base.Raid{ + { + Name: "some-name", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrRaidSupport, + path.New("yaml", "storage", "raid"), + }, + }, + }, + } + 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.AddOnError(entry.Path, entry.Err) + } + actual, translations, r := test.In.ToIgn3_3Unvalidated(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/butane/config/r4e/v1_1/schema.go b/butane/config/r4e/v1_1/schema.go new file mode 100644 index 000000000..beb16f4c6 --- /dev/null +++ b/butane/config/r4e/v1_1/schema.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_5" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/r4e/v1_1/translate.go b/butane/config/r4e/v1_1/translate.go new file mode 100644 index 000000000..a54ce16f0 --- /dev/null +++ b/butane/config/r4e/v1_1/translate.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "kernelArguments": common.ErrGeneralKernelArgumentSupport, + "storage.disks": common.ErrDiskSupport, + "storage.filesystems": common.ErrFilesystemSupport, + "storage.luks": common.ErrLuksSupport, + "storage.raid": common.ErrRaidSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// 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 +} + +// ToIgn3_4Bytes translates from a v1.1 Butane config to a v3.4.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 ToIgn3_4Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) +} diff --git a/butane/config/r4e/v1_1/translate_test.go b/butane/config/r4e/v1_1/translate_test.go new file mode 100644 index 000000000..801c8428c --- /dev/null +++ b/butane/config/r4e/v1_1/translate_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_1 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_5" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test that we error on unsupported fields for r4e +func TestTranslateInvalid(t *testing.T) { + type InvalidEntry struct { + Kind report.EntryKind + Err error + Path path.ContextPath + } + tests := []struct { + In Config + Entries []InvalidEntry + }{ + // we don't support setting kernel arguments + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // we don't support unsetting kernel arguments either + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldNotExist: []base.KernelArgument{ + "another-test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // disk customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "some-device", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrDiskSupport, + path.New("yaml", "storage", "disks"), + }, + }, + }, + // filesystem customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-label/TEST", + Path: util.StrToPtr("/var"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrFilesystemSupport, + path.New("yaml", "storage", "filesystems"), + }, + }, + }, + // default luks configuration is made in Image Builder for r4e, we don't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Label: util.StrToPtr("some-label"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrLuksSupport, + path.New("yaml", "storage", "luks"), + }, + }, + }, + // we don't support configuring raid via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Raid: []base.Raid{ + { + Name: "some-name", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrRaidSupport, + path.New("yaml", "storage", "raid"), + }, + }, + }, + } + 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.AddOnError(entry.Path, entry.Err) + } + actual, translations, r := test.In.ToIgn3_4Unvalidated(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/butane/config/r4e/v1_2_exp/schema.go b/butane/config/r4e/v1_2_exp/schema.go new file mode 100644 index 000000000..a10b62609 --- /dev/null +++ b/butane/config/r4e/v1_2_exp/schema.go @@ -0,0 +1,23 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2_exp + +import ( + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" +) + +type Config struct { + base.Config `yaml:",inline"` +} diff --git a/butane/config/r4e/v1_2_exp/translate.go b/butane/config/r4e/v1_2_exp/translate.go new file mode 100644 index 000000000..e1bbc541d --- /dev/null +++ b/butane/config/r4e/v1_2_exp/translate.go @@ -0,0 +1,54 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2_exp + +import ( + "github.com/coreos/ignition/v2/butane/config/common" + cutil "github.com/coreos/ignition/v2/butane/config/util" + + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/vcontext/report" +) + +var ( + fieldFilters = cutil.NewFilters(types.Config{}, cutil.FilterMap{ + "kernelArguments": common.ErrGeneralKernelArgumentSupport, + "storage.disks": common.ErrDiskSupport, + "storage.filesystems": common.ErrFilesystemSupport, + "storage.luks": common.ErrLuksSupport, + "storage.raid": common.ErrRaidSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToIgn3_7 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_7(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_7Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToIgn3_7Bytes translates from a v1.2 Butane config to a v3.5.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 ToIgn3_7Bytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_7", options) +} diff --git a/butane/config/r4e/v1_2_exp/translate_test.go b/butane/config/r4e/v1_2_exp/translate_test.go new file mode 100644 index 000000000..189b4bee8 --- /dev/null +++ b/butane/config/r4e/v1_2_exp/translate_test.go @@ -0,0 +1,180 @@ +// Copyright 2022 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_2_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/ignition/v2/butane/base/util" + base "github.com/coreos/ignition/v2/butane/base/v0_8_exp" + "github.com/coreos/ignition/v2/butane/config/common" + confutil "github.com/coreos/ignition/v2/butane/config/util" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// Test that we error on unsupported fields for r4e +func TestTranslateInvalid(t *testing.T) { + type InvalidEntry struct { + Kind report.EntryKind + Err error + Path path.ContextPath + } + tests := []struct { + In Config + Entries []InvalidEntry + }{ + // we don't support setting kernel arguments + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // we don't support unsetting kernel arguments either + { + Config{ + Config: base.Config{ + KernelArguments: base.KernelArguments{ + ShouldNotExist: []base.KernelArgument{ + "another-test", + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrGeneralKernelArgumentSupport, + path.New("yaml", "kernel_arguments"), + }, + }, + }, + // disk customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Disks: []base.Disk{ + { + Device: "some-device", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrDiskSupport, + path.New("yaml", "storage", "disks"), + }, + }, + }, + // filesystem customizations are made in Image Builder, r4e doesn't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/disk/by-label/TEST", + Path: util.StrToPtr("/var"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrFilesystemSupport, + path.New("yaml", "storage", "filesystems"), + }, + }, + }, + // default luks configuration is made in Image Builder for r4e, we don't support this via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Label: util.StrToPtr("some-label"), + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrLuksSupport, + path.New("yaml", "storage", "luks"), + }, + }, + }, + // we don't support configuring raid via ignition + { + Config{ + Config: base.Config{ + Storage: base.Storage{ + Raid: []base.Raid{ + { + Name: "some-name", + }, + }, + }, + }, + }, + []InvalidEntry{ + { + report.Error, + common.ErrRaidSupport, + path.New("yaml", "storage", "raid"), + }, + }, + }, + } + 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.AddOnError(entry.Path, entry.Err) + } + actual, translations, r := test.In.ToIgn3_7Unvalidated(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/butane/config/util/filter.go b/butane/config/util/filter.go new file mode 100644 index 000000000..3e015bf41 --- /dev/null +++ b/butane/config/util/filter.go @@ -0,0 +1,186 @@ +// Copyright 2023 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 util + +import ( + "fmt" + "reflect" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +type FilterMap map[string]error + +type FieldFilters struct { + filters FilterMap + // openshift 4.8 and 4.9 specs want to filter out the compression + // field but ignore a pointer to a zero value (StrToPtr("")) + // because those are generated automatically by desugaring. Provide + // a way to do that. + ignoreZero map[string]struct{} +} + +func NewFilters(v any, filters FilterMap) FieldFilters { + return NewFiltersIgnoreZero(v, filters, []string{}) +} + +func NewFiltersIgnoreZero(v any, filters FilterMap, ignoreZero []string) FieldFilters { + for filter := range filters { + if !isValidFilter(reflect.TypeOf(v), filter) { + panic(fmt.Errorf("invalid filter path: %s", filter)) + } + } + ignore := make(map[string]struct{}) + for _, value := range ignoreZero { + ignore[value] = struct{}{} + } + return FieldFilters{ + filters: filters, + ignoreZero: ignore, + } +} + +func isValidFilter(typ reflect.Type, filter string) bool { + if filter == "" { + return true + } + kind := typ.Kind() + switch { + case util.IsPrimitive(kind): + // can't descend further + return false + case kind == reflect.Struct: + element, rest, _ := strings.Cut(filter, ".") + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if field.Anonymous { + if isValidFilter(field.Type, filter) { + return true + } + } else { + if getTag(field) == element { + return isValidFilter(field.Type, rest) + } + } + } + return false + case kind == reflect.Slice, kind == reflect.Ptr: + return isValidFilter(typ.Elem(), filter) + default: + panic(fmt.Errorf("%v has kind %v", typ.Name(), kind)) + } +} + +func (ff FieldFilters) Verify(v any) report.Report { + return ff.verify(reflect.ValueOf(v), "", path.New("json")) +} + +func (ff FieldFilters) verify(v reflect.Value, filter string, p path.ContextPath) (r report.Report) { + if err := ff.Lookup(filter); err != nil { + // This object is filtered. Add an error if it's non-empty, + // but don't descend further in any case. + if !ff.isEmpty(v, filter) { + r.AddOnError(p, err) + } + return + } + + typ := v.Type() + kind := typ.Kind() + switch { + case util.IsPrimitive(kind): + case kind == reflect.Struct: + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if typ.Field(i).Anonymous { + r.Merge(ff.verify(field, filter, p)) + } else { + tag := getTag(typ.Field(i)) + r.Merge(ff.verify(field, fmt.Sprintf("%s.%s", filter, tag), p.Append(tag))) + } + } + case kind == reflect.Slice: + for i := 0; i < v.Len(); i++ { + r.Merge(ff.verify(v.Index(i), filter, p.Append(i))) + } + case kind == reflect.Ptr: + if !v.IsNil() { + r.Merge(ff.verify(v.Elem(), filter, p)) + } + case kind == reflect.Map: + // not supported in filters; ignore + default: + panic(fmt.Errorf("%v has kind %v", typ.Name(), kind)) + } + return +} + +func (ff FieldFilters) isEmpty(v reflect.Value, filter string) bool { + typ := v.Type() + kind := typ.Kind() + switch { + case util.IsPrimitive(kind): + return v.IsZero() + case kind == reflect.Struct: + for i := 0; i < v.NumField(); i++ { + childFilter := filter + if !typ.Field(i).Anonymous { + childFilter = fmt.Sprintf("%s.%s", filter, getTag(typ.Field(i))) + } + if !ff.isEmpty(v.Field(i), childFilter) { + return false + } + } + return true + case kind == reflect.Slice: + // different from v.IsZero(): we treat a non-nil zero-length + // slice as empty + return v.Len() == 0 + case kind == reflect.Ptr: + if v.IsNil() { + return true + } + // special case: if pointing to a primitive, and the primitive + // is the zero value, and the filter is listed in ff.ignoreZero, + // treat as empty + if util.IsPrimitive(typ.Elem().Kind()) && v.Elem().IsZero() { + _, ignoreZero := ff.ignoreZero[strings.TrimPrefix(filter, ".")] + if ignoreZero { + return true + } + } + return false + case kind == reflect.Map: + // not supported in filters; prune + return true + default: + panic(fmt.Errorf("%v has kind %v", typ.Name(), kind)) + } +} + +func (ff FieldFilters) Lookup(filter string) error { + return ff.filters[strings.TrimPrefix(filter, ".")] +} + +func getTag(field reflect.StructField) string { + tag, ok := field.Tag.Lookup("json") + if !ok { + panic(fmt.Errorf("struct field %q has no JSON tag", field.Name)) + } + return strings.Split(tag, ",")[0] +} diff --git a/butane/config/util/filter_test.go b/butane/config/util/filter_test.go new file mode 100644 index 000000000..6d737ae34 --- /dev/null +++ b/butane/config/util/filter_test.go @@ -0,0 +1,153 @@ +// Copyright 2023 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 util + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +var ( + ErrB = fmt.Errorf("err B") + ErrI = fmt.Errorf("err I") + ErrS = fmt.Errorf("err S") + ErrSB = fmt.Errorf("err SB") + ErrSSB = fmt.Errorf("err SSB") + ErrBP = fmt.Errorf("err BP") + ErrIP = fmt.Errorf("err IP") + ErrSP = fmt.Errorf("err SP") + ErrF = fmt.Errorf("err F") +) + +type StrA struct { + B bool `json:"b"` + I int `json:"i"` + S string `json:"s"` + SB StrB `json:"sb"` + SSB []StrB `json:"ssb"` +} + +type StrB struct { + BP *bool `json:"bp"` + IP *int `json:"ip"` + SP *string `json:"sp"` + StrC +} + +type StrC struct { + F float64 `json:"f"` +} + +func TestNewFilters(t *testing.T) { + NewFilters(StrA{}, FilterMap{ + "b": ErrB, + "i": ErrI, + "s": ErrS, + "sb.bp": ErrBP, + "ssb.ip": ErrIP, + "sb.f": ErrF, + }) + assert.Panics(t, func() { + NewFilters(StrA{}, FilterMap{ + "z": ErrB, + }) + }) + assert.Panics(t, func() { + NewFilters(StrA{}, FilterMap{ + "ssb.z": ErrB, + }) + }) + assert.Panics(t, func() { + NewFilters(StrA{}, FilterMap{ + "ssb.ip.z": ErrB, + }) + }) + assert.Panics(t, func() { + NewFilters(StrA{}, FilterMap{ + "sb.f.z": ErrB, + }) + }) +} + +func TestFilter(t *testing.T) { + obj := StrA{ + I: 7, + S: "hello", + SB: StrB{ + BP: util.BoolToPtr(true), + IP: util.IntToPtr(7), + SP: util.StrToPtr("goodbye"), + StrC: StrC{ + F: 3.1, + }, + }, + SSB: []StrB{ + { + BP: util.BoolToPtr(true), + }, + { + SP: util.StrToPtr("str"), + }, + { + SP: util.StrToPtr(""), + }, + }, + } + + // no filters, no errors + assert.Equal(t, report.Report{}, NewFilters(StrA{}, FilterMap{}).Verify(obj)) + + // various filters, ignore zero + var expected report.Report + expected.AddOnError(path.New("json", "sb", "ip"), ErrIP) + expected.AddOnError(path.New("json", "sb", "f"), ErrF) + expected.AddOnError(path.New("json", "ssb", 0, "bp"), ErrBP) + expected.AddOnError(path.New("json", "ssb", 1, "sp"), ErrSP) + assert.Equal(t, expected, NewFiltersIgnoreZero(StrA{}, FilterMap{ + "b": ErrB, + "sb.ip": ErrIP, + "sb.f": ErrF, + "ssb.bp": ErrBP, + "ssb.ip": ErrIP, + "ssb.sp": ErrSP, + }, []string{ + "ssb.sp", + }).Verify(obj)) + // stop ignoring zero + expected.AddOnError(path.New("json", "ssb", 2, "sp"), ErrSP) + assert.Equal(t, expected, NewFilters(StrA{}, FilterMap{ + "b": ErrB, + "sb.ip": ErrIP, + "sb.f": ErrF, + "ssb.bp": ErrBP, + "ssb.ip": ErrIP, + "ssb.sp": ErrSP, + }).Verify(obj)) + + // filter stops descent + expected = report.Report{} + expected.AddOnError(path.New("json", "ssb"), ErrSSB) + assert.Equal(t, expected, NewFilters(StrA{}, FilterMap{ + "ssb": ErrSSB, + "ssb.bp": ErrBP, + "ssb.ip": ErrIP, + "ssb.sp": ErrSP, + }).Verify(obj)) +} diff --git a/butane/config/util/util.go b/butane/config/util/util.go new file mode 100644 index 000000000..fc6de99db --- /dev/null +++ b/butane/config/util/util.go @@ -0,0 +1,279 @@ +// Copyright 2019 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 util + +import ( + "bytes" + "fmt" + "os" + "reflect" + "regexp" + "strings" + "unicode" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/clarketm/json" + ignvalidate "github.com/coreos/ignition/v2/config/validate" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/coreos/vcontext/tree" + "github.com/coreos/vcontext/validate" + vyaml "github.com/coreos/vcontext/yaml" + "gopkg.in/yaml.v3" +) + +var ( + snakeRe = regexp.MustCompile("(MiB|[A-Z])") +) + +// Misc helpers + +type Config interface { + FieldFilters() *FieldFilters +} + +// Translate translates cfg to the corresponding Ignition config version +// using the named translation method on cfg, and returns the marshaled +// 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 Translate(cfg Config, translateMethod string, options common.TranslateOptions) (interface{}, report.Report, error) { + // Get method, and zero return value for error returns. + method := reflect.ValueOf(cfg).MethodByName(translateMethod) + zeroValue := reflect.Zero(method.Type().Out(0)).Interface() + + // Validate the input. + r := validate.Validate(cfg, "yaml") + if r.IsFatal() { + return zeroValue, r, common.ErrInvalidSourceConfig + } + + // Perform the translation. + translateRet := method.Call([]reflect.Value{reflect.ValueOf(options)}) + final := translateRet[0].Interface() + translations := translateRet[1].Interface().(translate.TranslationSet) + translateReport := translateRet[2].Interface().(report.Report) + r.Merge(TranslateReportPaths(translateReport, translations)) + if r.IsFatal() { + return zeroValue, r, common.ErrInvalidSourceConfig + } + if options.DebugPrintTranslations { + fmt.Fprint(os.Stderr, translations) + if err := translations.DebugVerifyCoverage(final); err != nil { + fmt.Fprintf(os.Stderr, "\n%s", err) + } + } + + // Check for fields forbidden by this spec. + filters := cfg.FieldFilters() + if filters != nil { + filterReport := filters.Verify(final) + r.Merge(TranslateReportPaths(filterReport, translations)) + if r.IsFatal() { + return zeroValue, r, common.ErrInvalidSourceConfig + } + } + + // Check for invalid duplicated keys. + dupsReport := validate.ValidateCustom(final, "json", ignvalidate.ValidateDups) + r.Merge(TranslateReportPaths(dupsReport, translations)) + + // Validate JSON semantics. + jsonReport := validate.Validate(final, "json") + r.Merge(TranslateReportPaths(jsonReport, translations)) + + if r.IsFatal() { + return zeroValue, r, common.ErrInvalidGeneratedConfig + } + return final, r, nil +} + +// TranslateBytes unmarshals the Butane config specified in input into the +// struct pointed to by container, translates it to the corresponding Ignition +// config version using the named translation method, and returns the +// marshaled 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 TranslateBytes(input []byte, container interface{}, translateMethod string, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + cfg := container + + // Unmarshal the YAML. + contextTree, err := unmarshal(input, cfg) + if err != nil { + return nil, report.Report{}, err + } + + // Check for unused keys. + unusedKeyCheck := func(v reflect.Value, c path.ContextPath) report.Report { + return ignvalidate.ValidateUnusedKeys(v, c, contextTree) + } + r := validate.ValidateCustom(cfg, "yaml", unusedKeyCheck) + r.Correlate(contextTree) + if r.IsFatal() { + return nil, r, common.ErrInvalidSourceConfig + } + + // Perform the translation. + translateRet := reflect.ValueOf(cfg).MethodByName(translateMethod).Call([]reflect.Value{reflect.ValueOf(options.TranslateOptions)}) + final := translateRet[0].Interface() + translateReport := translateRet[1].Interface().(report.Report) + errVal := translateRet[2] + translateReport.Correlate(contextTree) + r.Merge(translateReport) + if !errVal.IsNil() { + return nil, r, errVal.Interface().(error) + } + if r.IsFatal() { + return nil, r, common.ErrInvalidSourceConfig + } + + // Marshal the JSON. + outbytes, err := marshal(final, options.Pretty) + return outbytes, r, err +} + +func TranslateBytesYAML(input []byte, container interface{}, translateMethod string, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + // marshal to JSON, unmarshal, remarshal to YAML. there's no other + // good way to respect the `json` struct tags. + // https://github.com/go-yaml/yaml/issues/424 + jsonCfg, r, err := TranslateBytes(input, container, translateMethod, options) + if err != nil { + return jsonCfg, r, err + } + + var ifaceCfg interface{} + if err := json.Unmarshal(jsonCfg, &ifaceCfg); err != nil { + return []byte{}, r, err + } + + var yamlCfgBuf bytes.Buffer + yamlCfgBuf.WriteString("# Generated by Butane; do not edit\n") + encoder := yaml.NewEncoder(&yamlCfgBuf) + encoder.SetIndent(2) + if err := encoder.Encode(ifaceCfg); err != nil { + return []byte{}, r, err + } + if err := encoder.Close(); err != nil { + return []byte{}, r, err + } + yamlCfg := bytes.Trim(yamlCfgBuf.Bytes(), "\n") + return yamlCfg, r, err +} + +// Report an ErrFieldElided warning for any non-zero top-level fields in the +// specified output struct. The caller will probably want to use +// translate.PrefixReport() to reparent the report into the right place in +// the `json` hierarchy, and then TranslateReportPaths() to map back into +// `yaml` space. +func CheckForElidedFields(struct_ interface{}) report.Report { + v := reflect.ValueOf(struct_) + t := v.Type() + if t.Kind() != reflect.Struct { + panic("struct type required") + } + var r report.Report + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.IsValid() && !f.IsZero() { + tag := strings.Split(t.Field(i).Tag.Get("json"), ",")[0] + r.AddOnWarn(path.New("json", tag), common.ErrFieldElided) + } + } + return r +} + +// unmarshal unmarshals the data to "to" and also returns a context tree for the source. +func unmarshal(data []byte, to interface{}) (tree.Node, error) { + dec := yaml.NewDecoder(bytes.NewReader(data)) + if err := dec.Decode(to); err != nil { + return nil, err + } + return vyaml.UnmarshalToContext(data) +} + +// marshal is a wrapper for marshaling to json with or without pretty-printing the output +func marshal(from interface{}, pretty bool) ([]byte, error) { + if pretty { + return json.MarshalIndent(from, "", " ") + } + return json.Marshal(from) +} + +// snakePath converts a path.ContextPath with camelCase elements and returns the +// same path but with snake_case elements instead +func snakePath(p path.ContextPath) path.ContextPath { + ret := path.New(p.Tag) + for _, part := range p.Path { + if str, ok := part.(string); ok { + ret = ret.Append(Snake(str)) + } else { + ret = ret.Append(part) + } + } + return ret +} + +// Snake converts from camelCase (not CamelCase) to snake_case +func Snake(in string) string { + return strings.ToLower(snakeRe.ReplaceAllString(in, "_$1")) +} + +// Camel converts from snake_case to camelCase +func Camel(in string) string { + if strings.HasSuffix(in, "_mib") { + in = strings.TrimSuffix(in, "_mib") + "MiB" + } + arr := []rune(in) + for i := range arr { + if i > 0 && arr[i-1] == '_' { + arr[i] = unicode.ToUpper(arr[i]) + } + } + return strings.ReplaceAll(string(arr), "_", "") +} + +// TranslateReportPaths takes a report with a mix of json (camelCase) and +// yaml (snake_case) paths, and a set of translation rules. It applies +// those rules and converts all json paths to snake-cased yaml. +func TranslateReportPaths(r report.Report, ts translate.TranslationSet) report.Report { + var ret report.Report + ret.Merge(r) + for i, ent := range ret.Entries { + context := ent.Context + if context.Tag == "yaml" { + continue + } + if t, ok := ts.Set[context.String()]; ok { + context = t.From + } else { + // Missing translation. As a fallback, convert + // camelCase path elements to snake_case and hope + // there's a 1:1 mapping between the YAML and JSON + // hierarchies. Notably, that's not true for + // MachineConfig output, since the Ignition config + // is reparented to a grandchild of the root. + // See also https://github.com/coreos/butane/issues/213. + // This is hacky (notably, it leaves context.Tag as + // `json`) but sometimes it's enough to help us find + // a Marker, and when it isn't, the path still + // provides some feedback to the user. + context = snakePath(context) + } + ret.Entries[i].Context = context + } + return ret +} diff --git a/butane/config/util/util_test.go b/butane/config/util/util_test.go new file mode 100644 index 000000000..fc744d92c --- /dev/null +++ b/butane/config/util/util_test.go @@ -0,0 +1,121 @@ +// Copyright 2019 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 util + +import ( + "fmt" + "testing" + + "github.com/coreos/ignition/v2/butane/config/common" + "github.com/coreos/ignition/v2/butane/translate" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestSnake(t *testing.T) { + tests := []struct { + in string + out string + }{ + {}, + { + "foo", + "foo", + }, + { + "snakeCase", + "snake_case", + }, + { + "longSnakeCase", + "long_snake_case", + }, + { + "snake_already", + "snake_already", + }, + { + "camelMiB", + "camel_mib", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + if Snake(test.in) != test.out { + t.Errorf("expected %q got %q", test.out, Snake(test.in)) + } + }) + } +} + +func TestCamel(t *testing.T) { + tests := []struct { + in string + out string + }{ + {}, + { + "foo", + "foo", + }, + { + "snake_case", + "snakeCase", + }, + { + "long_snake_case", + "longSnakeCase", + }, + { + "camelAlready", + "camelAlready", + }, + { + "snake_mib", + "snakeMiB", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + if Camel(test.in) != test.out { + t.Errorf("expected %q got %q", test.out, Camel(test.in)) + } + }) + } +} + +func TestTranslateReportPaths(t *testing.T) { + ts := translate.NewTranslationSet("yaml", "json") + ts.AddTranslation(path.New("yaml", "a", "b", "c"), path.New("json", "d", "e", "f")) + makeReport := func(source bool) report.Report { + var r report.Report + var p path.ContextPath + if source { + p = path.New("yaml", "a", "b", "c") + } else { + p = path.New("json", "d", "e", "f") + } + r.AddOnError(p, common.ErrDecimalMode) + return r + } + r := makeReport(false) + r2 := TranslateReportPaths(r, ts) + assert.Equal(t, makeReport(false), r, "TranslateReportPaths changed original report") + assert.Equal(t, makeReport(true), r2, "TranslateReportPaths returned incorrect report") +} diff --git a/butane/docs/_config.yml b/butane/docs/_config.yml new file mode 100644 index 000000000..b4a7b566a --- /dev/null +++ b/butane/docs/_config.yml @@ -0,0 +1,48 @@ +# Template generated by https://github.com/coreos/repo-templates; do not edit downstream + +# To test documentation changes locally or using GitHub Pages, see: +# https://github.com/coreos/fedora-coreos-tracker/blob/main/docs/testing-project-documentation-changes.md + +title: Butane +description: Butane documentation +baseurl: "/butane" +url: "https://coreos.github.io" +permalink: /:title/ +markdown: kramdown +kramdown: + typographic_symbols: + ndash: "--" + mdash: "---" + +remote_theme: just-the-docs/just-the-docs@v0.12.0 +plugins: + - jekyll-remote-theme + +color_scheme: coreos + +# Aux links for the upper right navigation +aux_links: + "Butane on GitHub": + - "https://github.com/coreos/butane" + +footer_content: "Copyright © Red Hat, Inc. and others." + +# Footer last edited timestamp +last_edit_timestamp: true +last_edit_time_format: "%b %e %Y at %I:%M %p" + +# Footer "Edit this page on GitHub" link text +gh_edit_link: true +gh_edit_link_text: "Edit this page on GitHub" +gh_edit_repository: "https://github.com/coreos/butane" +gh_edit_branch: "main" +gh_edit_source: docs +gh_edit_view_mode: "tree" + +compress_html: + clippings: all + comments: all + endings: all + startings: [] + blanklines: false + profile: false diff --git a/butane/docs/_sass/color_schemes/coreos.scss b/butane/docs/_sass/color_schemes/coreos.scss new file mode 100644 index 000000000..a4554be88 --- /dev/null +++ b/butane/docs/_sass/color_schemes/coreos.scss @@ -0,0 +1 @@ +$link-color: #53a3da; diff --git a/butane/docs/_sass/custom/custom.scss b/butane/docs/_sass/custom/custom.scss new file mode 100644 index 000000000..e8fa84f28 --- /dev/null +++ b/butane/docs/_sass/custom/custom.scss @@ -0,0 +1,9 @@ +#spec-docs ~ ul { + ul { + border-left: 1px solid $grey-lt-300; + } + + li::before { + margin-left: -0.8em; + } +} diff --git a/butane/docs/config-fcos-v1_0.md b/butane/docs/config-fcos-v1_0.md new file mode 100644 index 000000000..e58f63373 --- /dev/null +++ b/butane/docs/config-fcos-v1_0.md @@ -0,0 +1,134 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.0.0 +parent: Configuration specifications +nav_order: 49 +--- + +# Fedora CoreOS Specification v1.0.0 + +The Fedora CoreOS 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 `fcos` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.0.0` and generates Ignition configs with version `3.0.0`. +* **_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`, 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. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `