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..13ba0d6de 100755 --- a/build +++ b/build @@ -35,3 +35,7 @@ NAME="ignition-validate" echo "Building ${NAME}..." go build -ldflags "${GLDFLAGS}" -o ${BIN_PATH}/${NAME} ${REPO_PATH}/validate + +# Build butane from the subtree (separate Go module) +echo "Building butane..." +(cd butane && BIN_PATH=${BIN_PATH} ./build) diff --git a/butane/.copr/Makefile b/butane/.copr/Makefile new file mode 100644 index 000000000..eb2c405b3 --- /dev/null +++ b/butane/.copr/Makefile @@ -0,0 +1,16 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +.PHONY: srpm +srpm: + dnf install -y git rpm-build rpmdevtools + # similar to https://github.com/actions/checkout/issues/760, but for COPR + git config --global --add safe.directory '*' + curl -LOf https://src.fedoraproject.org/rpms/butane/raw/rawhide/f/butane.spec + version=$$(git describe --always --tags | sed -e 's,-,\.,g' -e 's,^v,,'); \ + git archive --format=tar --prefix=butane-$$version/ HEAD | gzip > butane-$$version.tar.gz; \ + sed -ie "s,^Version:.*,Version: $$version," butane.spec + sed -ie 's/^Patch/# Patch/g' butane.spec # we don't want any downstream patches + spectool -g butane.spec # download any remaining sources (e.g. coreos-installer-dracut) + rpmbuild -bs --define "_sourcedir ${PWD}" --define "_specdir ${PWD}" --define "_builddir ${PWD}" --define "_srcrpmdir ${PWD}" --define "_rpmdir ${PWD}" --define "_buildrootdir ${PWD}/.build" butane.spec + mv *.src.rpm $$outdir diff --git a/butane/.fmf/version b/butane/.fmf/version new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/butane/.fmf/version @@ -0,0 +1 @@ +1 diff --git a/butane/.gemini/config.yaml b/butane/.gemini/config.yaml new file mode 100644 index 000000000..f742bc15f --- /dev/null +++ b/butane/.gemini/config.yaml @@ -0,0 +1,13 @@ +# This config mainly overrides `summary: false` by default +# as it's really noisy. +have_fun: true +code_review: + disable: false + comment_severity_threshold: "MEDIUM" + max_review_comments: -1 + pull_request_opened: + help: false + # Turned off by default + summary: false + code_review: true +ignore_patterns: [] diff --git a/butane/.gitattributes b/butane/.gitattributes new file mode 100644 index 000000000..d207b1802 --- /dev/null +++ b/butane/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/butane/.github/ISSUE_TEMPLATE/release-checklist.md b/butane/.github/ISSUE_TEMPLATE/release-checklist.md new file mode 100644 index 000000000..f5ab7d439 --- /dev/null +++ b/butane/.github/ISSUE_TEMPLATE/release-checklist.md @@ -0,0 +1,88 @@ +--- +name: release checklist +about: release checklist template +title: New release for butane +labels: jira,kind/release +warning: | + ⚠️ Template generated by https://github.com/coreos/repo-templates; do not edit downstream +--- + +Release checklist: + +Tagging: + - [ ] Write release notes in `docs/release-notes.md`. Get them reviewed and merged + - [ ] If the release signing key has changed because a new Fedora release has gone stable, note the change as done [here](https://github.com/coreos/butane/releases/tag/v0.12.0). + - [ ] If doing a branched release, also include a PR to merge the `docs/release-notes.md` changes into main + - [ ] Ensure your local copy is up to date with the upstream main branch (`git@github.com:coreos/butane.git`) + - [ ] Ensure your working directory is clean (`git clean -fdx`) + - [ ] Ensure you can sign commits and any yubikeys/smartcards are plugged in + - [ ] Run `./tag_release.sh ` + - [ ] Push that tag to GitHub + +Fedora packaging: + - [ ] Update the spec file in [Fedora](https://src.fedoraproject.org/rpms/butane): + - Bump the `Version` + - Switch the `Release` back to `1%{?dist}` + - Remove any patches obsoleted by the new release + - Run `go-mods-to-bundled-provides.py | sort` while inside of the `butane` directory you ran `./tag_release` from & copy output into spec file in `# Main package provides` section + - Update changelog + - [ ] Run `spectool -g -S butane.spec` + - [ ] Run `kinit your_fas_account@FEDORAPROJECT.ORG` + - [ ] Run `fedpkg new-sources $(spectool -S butane.spec | sed 's:.*/::')` + - [ ] PR the changes in [Fedora](https://src.fedoraproject.org/rpms/butane) + - [ ] Once the PR merges to rawhide, merge rawhide into the other relevant branches (e.g. f44) then push those, for example: + ```bash + git checkout rawhide + git pull --ff-only + git checkout f44 + git merge --ff-only rawhide + git push origin f44 + ``` + - [ ] On each of those branches run `fedpkg build` including rawhide. + - [ ] Once the builds have finished, submit them to [bodhi](https://bodhi.fedoraproject.org/updates/new), filling in: + - `butane` for `Packages` + - Selecting the build(s) that just completed, except for the rawhide one (which gets submitted automatically) + - Writing brief release notes like "New upstream release; see release notes at `link to docs/release-notes.md on GH tag`" + - Leave `Update name` blank + - `Type`, `Severity` and `Suggestion` can be left as `unspecified` unless it is a security release. In that case select `security` with the appropriate severity. + - `Stable karma` and `Unstable` karma can be set to `2` and `-1`, respectively. + +GitHub release: + - [ ] Wait until the Bodhi update shows "Signed :heavy_check_mark:" in the Metadata box. + - [ ] Verify that the signing script can fetch the release binaries by running `./signing-ticket.sh test `, where `r` is the Release of the Fedora package without the dist tag (probably `1`) + - [ ] Run `./signing-ticket.sh ticket ` and paste the output into a [releng ticket](https://forge.fedoraproject.org/releng/tickets/issues/new). + - [ ] Wait for the ticket to be closed + - [ ] Download the artifacts and signatures + - [ ] Verify the signatures + - [ ] Find the new tag in the [GitHub tag list](https://github.com/coreos/butane/tags) and click the triple dots menu, and create a draft release for it. + - [ ] Copy and paste the release notes from `docs/release-notes.md` + - [ ] Upload all the release artifacts and their signatures + - [ ] Publish the release + +Quay release: + - [ ] Visit the [Quay tags page](https://quay.io/repository/coreos/butane?tab=tags) and wait for a versioned tag to appear + - [ ] Click the gear next to the tag, select "Add New Tag", enter `release`, and confirm + - [ ] Visit the [Quay tags page](https://quay.io/repository/coreos/fcct?tab=tags) for the legacy `coreos/fcct` repo and wait for a versioned tag to appear + - [ ] Click the gear next to the tag, select "Add New Tag", enter `release`, and confirm + +RHCOS packaging for the current RHCOS development release: + - [ ] Update the [spec file](https://gitlab.com/redhat/rhel/rpms/butane) + - Bump the `Version` + - Switch the `Release` back to `1%{?dist}` + - Remove any patches obsoleted by the new release + - Run `go-mods-to-bundled-provides.py | sort` while inside of the `butane` directory you ran `./tag_release` from & copy output into spec file in `# Main package provides` section + - Update changelog + - [ ] Run `spectool -g -S butane.spec` + - [ ] Run `kinit your_account@IPA.REDHAT.COM` + - [ ] Run `rhpkg new-sources $(spectool -S butane.spec | sed 's:.*/::')` + - [ ] PR the changes + - [ ] Get the PR reviewed and merge it + - [ ] Update your local repo and run `rhpkg build` + - [ ] File ticket similar to [this one](https://issues.redhat.com/browse/ART-3711) to sync the new version to mirror.openshift.com + - [ ] Wait until mirror.openshift.com is updated and confirm the new version is correct + +CentOS Stream 9 packaging: + - [ ] Create a `rebase-c9s-butane` issue in the internal team-operations repo and follow the steps there + +CentOS Stream 10 packaging: + - [ ] Create a `rebase-c10s-butane` issue in the internal team-operations repo and follow the steps there diff --git a/butane/.github/ISSUE_TEMPLATE/stabilize-checklist.md b/butane/.github/ISSUE_TEMPLATE/stabilize-checklist.md new file mode 100644 index 000000000..6ef9c4ea1 --- /dev/null +++ b/butane/.github/ISSUE_TEMPLATE/stabilize-checklist.md @@ -0,0 +1,43 @@ +--- +name: Stabilization checklist +about: Stabilization checklist template +title: New stabilization for Butane +labels: jira +--- + +# Bumping spec versions + +This checklist describes bumping the Ignition spec version, `base` version, and distro versions. If your scenario is different, modify to taste. + +## Stabilize Ignition spec version + +- [ ] Bump `go.mod` for new Ignition release and update vendor. +- [ ] Update imports. Drop `-experimental` from Ignition spec versions in `*/translate_test.go`. + +## Bump base version + +- [ ] 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`. + +## Bump distro version + +- [ ] Rename `config/distro/vD_exp` to `config/distro/vD` and update `package` statements. Update imports. +- [ ] Drop `-experimental` from `init()` in `config/config.go`. +- [ ] Drop `-experimental` from examples in `docs/`. +- [ ] Copy `config/distro/vD` to `config/distro/vD+1_exp`. +- [ ] Update `package` statements in `config/distro/vD+1_exp`. Bump its base dependency to `base/vB+1_exp`. +- [ ] Import `config/vD+1_exp` in `config/config.go` and add `distro` `C+1-experimental` to `init()`. + +## Bump Ignition spec version + +- [ ] Bump Ignition types imports and rename `ToIgnI` and `TestToIgnI` functions in `base/vB+1_exp`. Bump Ignition spec versions in `base/vB+1_exp/translate_test.go`. +- [ ] Bump Ignition types imports in `config/distro/vD+1_exp`. Update `ToIgnI` function names, `util` calls, and header comments to `ToIgnI+1`. + +## Update docs + +- [ ] Update `internal/doc/main.go` to add the new stable spec and reference the new experimental spec in `generate()`. +- [ ] Run `generate` to regenerate spec docs. +- [ ] Update `docs/specs.md`. +- [ ] Update `docs/upgrading-*.md` for the new spec version. Copy the relevant section from Ignition's `doc/migrating-configs.md`, convert the configs to Butane configs, convert field names to snake case, and update wording as needed. Add subsections for any new Butane-specific features. +- [ ] Note the stabilization in `docs/release-notes.md`, following the format of previous stabilizations. Drop the `-exp` version suffix from any notes for the upcoming release. diff --git a/butane/.github/dependabot.yml b/butane/.github/dependabot.yml new file mode 100644 index 000000000..53b7de6c1 --- /dev/null +++ b/butane/.github/dependabot.yml @@ -0,0 +1,22 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +# Updates are grouped together by ecosystem in a single PR. An update can be +# removed from a combined update PR via comments to dependabot: +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates#managing-dependabot-pull-requests-for-grouped-updates-with-comment-commands + +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + labels: + - dependency + - skip-notes + + groups: + build: + patterns: + - "*" diff --git a/butane/.github/workflows/container-rebuild.yml b/butane/.github/workflows/container-rebuild.yml new file mode 100644 index 000000000..f14107797 --- /dev/null +++ b/butane/.github/workflows/container-rebuild.yml @@ -0,0 +1,46 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +name: Rebuild release container + +on: + workflow_dispatch: + inputs: + git-tag: + description: Existing Git tag + default: vX.Y.Z + docker-tag: + description: New Docker versioned tag + default: vX.Y.Z-1 + +permissions: + contents: read + +# avoid races when pushing containers built from main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + build-container: + name: Build container image + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.git-tag }} + # fetch tags so the compiled-in version number is useful + fetch-depth: 0 + # If we're running on a signed tag, actions/checkout rewrites it into + # a lightweight tag (!!!) which "git describe" then ignores. Rewrite + # it back. + # https://github.com/actions/checkout/issues/290 + - name: Fix actions/checkout synthetic tag + run: git fetch --tags --force + - name: Build and push container + uses: coreos/actions-lib/build-container@main + with: + credentials: ${{ secrets.QUAY_AUTH }} + push: quay.io/coreos/butane quay.io/coreos/fcct + arches: amd64 arm64 + tags: ${{ github.event.inputs.docker-tag }} release diff --git a/butane/.github/workflows/container.yml b/butane/.github/workflows/container.yml new file mode 100644 index 000000000..a909995d1 --- /dev/null +++ b/butane/.github/workflows/container.yml @@ -0,0 +1,43 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +name: Container + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +permissions: + contents: read + +# avoid races when pushing containers built from main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + build-container: + name: Build container image + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + # fetch tags so the compiled-in version number is useful + fetch-depth: 0 + # If we're running on a signed tag, actions/checkout rewrites it into + # a lightweight tag (!!!) which "git describe" then ignores. Rewrite + # it back. + # https://github.com/actions/checkout/issues/290 + - name: Fix actions/checkout synthetic tag + run: git fetch --tags --force + - name: Build and push container + uses: coreos/actions-lib/build-container@main + with: + credentials: ${{ secrets.QUAY_AUTH }} + push: quay.io/coreos/butane quay.io/coreos/fcct + arches: amd64 arm64 + # Speed up PR CI by skipping non-amd64 + pr-arches: amd64 diff --git a/butane/.github/workflows/go.yml b/butane/.github/workflows/go.yml new file mode 100644 index 000000000..6a6728d5c --- /dev/null +++ b/butane/.github/workflows/go.yml @@ -0,0 +1,83 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +name: Go +on: + push: + branches: [main] + pull_request: + branches: [main] +permissions: + contents: read + +# don't waste job slots on superseded code +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + strategy: + matrix: + go-version: [1.25.x, 1.26.x] + os: [ubuntu-latest] + include: + - go-version: 1.26.x + os: macos-latest + - go-version: 1.26.x + os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Check out repository + uses: actions/checkout@v6 + - name: Install macOS dependencies + if: runner.os == 'macOS' + shell: bash + run: brew install coreutils + - name: Check modules + run: go mod verify + - name: Test + shell: bash + run: ./test + - name: Check Go formatting (gofmt) + if: runner.os == 'Linux' + shell: bash + run: | + GO_FILES=$(find . -name '*.go' -not -path "./vendor/*") + UNFORMATTED_FILES=$(gofmt -l $GO_FILES) + if [ -n "$UNFORMATTED_FILES" ]; then + echo "Go files are not formatted. Please run 'gofmt -w .' on your code." + gofmt -d $UNFORMATTED_FILES + exit 1 + fi + echo "All Go files are correctly formatted." + - name: Run linter + uses: golangci/golangci-lint-action@v8 + if: runner.os == 'Linux' + with: + version: v2.11.3 + regenerate: + name: Regenerate + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v6 + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: 1.26.x + - name: Regenerate + run: ./generate + - name: Check whether generated output is current + run: | + if [ -n "$(git status --porcelain docs)" ]; then + echo "Found local changes after regenerating:" + git --no-pager diff --color=always docs + echo "Rerun './generate'." + exit 1 + fi diff --git a/butane/.github/workflows/issue-eval.yml b/butane/.github/workflows/issue-eval.yml new file mode 100644 index 000000000..feda9065b --- /dev/null +++ b/butane/.github/workflows/issue-eval.yml @@ -0,0 +1,22 @@ +name: issue-eval + +on: + issues: + types: [opened] + +permissions: + issues: write + contents: read + +jobs: + process-issue: + runs-on: ubuntu-latest + steps: + - name: Run issue-eval + uses: prestist/issue-eval@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + google-ai-api-key: ${{ secrets.GOOGLE_AI_API_KEY }} + issue-number: ${{ github.event.issue.number }} + repo-owner: ${{ github.repository_owner }} + repo-name: ${{ github.event.repository.name }} \ No newline at end of file diff --git a/butane/.github/workflows/require-release-note.yml b/butane/.github/workflows/require-release-note.yml new file mode 100644 index 000000000..611fb5acb --- /dev/null +++ b/butane/.github/workflows/require-release-note.yml @@ -0,0 +1,27 @@ +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +name: Release notes + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + +concurrency: + group: release-note-${{ github.ref }} + cancel-in-progress: true + +jobs: + require-notes: + name: Require release note + runs-on: ubuntu-latest + steps: + - name: Require release-notes.md update + uses: coreos/actions-lib/require-file-change@main + with: + path: docs/release-notes.md + override-label: skip-notes diff --git a/butane/.github/workflows/tmt-tests.yml b/butane/.github/workflows/tmt-tests.yml new file mode 100644 index 000000000..6adf36d25 --- /dev/null +++ b/butane/.github/workflows/tmt-tests.yml @@ -0,0 +1,75 @@ +name: TMT Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + inputs: + plan_filter: + description: | + Test plan filter name, ie: tag:smoke. + If provided, only tests matching this filter will be run, otherwise all tests will be run. + From the TMT help: + Apply an advanced filter using key:value + pairs and logical operators. For example + 'tier:1 & tag:core'. Use the 'name' key to + search by name. See 'pydoc fmf.filter' for + detailed documentation on the syntax + required: false + default: '' + use_built_from_src: + description: 'Built butane from source instead of install distro package' + required: false + default: 'true' + +jobs: + tmt-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: 'stable' + + - name: Set additional paths + run: | + set -x -e -o pipefail + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install dependencies + run: | + set -x -e -o pipefail + sudo apt-get update + sudo apt-get install -y podman libblkid-dev rsync + pip install --user tmt + - name: Build butane + if: github.event.inputs.use_built_from_src == '' || github.event.inputs.use_built_from_src == 'true' + run: | + set -x -e -o pipefail + ./build_for_container + - name: Run TMT tests + run: | + set -x -e -o pipefail + if [ "$ACT" = "true" ]; then + echo "Running locally using ACT" # ACT ref: https://github.com/nektos/act + TMT_PROVISION_OPTS="--how local --feeling-safe" + else + TMT_PROVISION_OPTS="--how container" + fi + if [ -n "${{ github.event.inputs.plan_filter }}" ]; then + PLAN_FILTER_PARAM="plan --filter '${{ github.event.inputs.plan_filter }}'" + fi + if [ -z "${{ github.event.inputs.use_built_from_src }}" ] || [ "${{ github.event.inputs.use_built_from_src }}" == "true" ]; then + CONTEXT_PARAM="--context use_built_from_src=true" + else + CONTEXT_PARAM="--context use_built_from_src=false" + fi + # eval is used to allow the use of variables in the command + # and to avoid issues withe the tmt --filter option + eval "tmt $CONTEXT_PARAM run --all --debug -vvvv provision $TMT_PROVISION_OPTS $PLAN_FILTER_PARAM" diff --git a/butane/.gitignore b/butane/.gitignore new file mode 100644 index 000000000..8375b8f6f --- /dev/null +++ b/butane/.gitignore @@ -0,0 +1,2 @@ +/bin +/tmpdocs diff --git a/butane/.golangci.yml b/butane/.golangci.yml new file mode 100644 index 000000000..4cccee282 --- /dev/null +++ b/butane/.golangci.yml @@ -0,0 +1,27 @@ +version: "2" +linters: + exclusions: + # Allow unkeyed fields in composite literals in tests + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - path: _test\.go$ + text: composite literal uses unkeyed fields + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ + enable: + - gofmt + \ No newline at end of file diff --git a/butane/.opencode/skills/add-sugar/SKILL.md b/butane/.opencode/skills/add-sugar/SKILL.md new file mode 100644 index 000000000..a10618aca --- /dev/null +++ b/butane/.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/skills/remove-feature/SKILL.md b/butane/.opencode/skills/remove-feature/SKILL.md new file mode 100644 index 000000000..5f7369781 --- /dev/null +++ b/butane/.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/skills/stabilize-spec/SKILL.md b/butane/.opencode/skills/stabilize-spec/SKILL.md new file mode 100644 index 000000000..79d20b5c1 --- /dev/null +++ b/butane/.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/AGENTS.md b/butane/AGENTS.md new file mode 100644 index 000000000..c87999142 --- /dev/null +++ b/butane/AGENTS.md @@ -0,0 +1,139 @@ +# Butane Repository + +**Butane** translates human-readable Butane Configs into machine-readable [Ignition](https://github.com/coreos/ignition) Configs for CoreOS-based operating systems. + +**Variants**: `fcos` (Fedora CoreOS), `flatcar`, `openshift`, `r4e` (RHEL for Edge), `fiot` (Fedora IoT) + +## Architecture + +``` +base/vX_Y/ # Distro-agnostic, targets Ignition versions (v0_7, v0_8_exp) +config/*/vX_Y/ # Distro-specific (fcos/v1_7, fcos/v1_8_exp, openshift/v4_21) + ├── schema.go # Butane config structs + ├── translate.go # Butane → Ignition translation + └── validate.go # Validation logic +internal/doc/ # Documentation generation (butane.yaml) +.opencode/ # Automation scripts and skills +``` + +**Rules**: +- Never import from `base/` packages directly +- Only experimental specs (`*_exp`) receive new features +- Stable specs are frozen (bug fixes only) + +**Version Lifecycle**: Experimental → Stabilization → Stable → New Experimental + +## Core Patterns + +### Schema-Translate-Validate + +Each spec implements three files: + +**schema.go**: Go structs with YAML tags, optional fields use pointers (`*string`, `*bool`), auto-filtered fields use `` `butane:"auto_skip"` `` + +**translate.go**: Main function `ToIgnX_YUnvalidated()` returns (config, translation set, report) + +**validate.go**: Returns validation reports (warnings/errors), uses `config/common/errors.go` for error definitions + +### Sugar Implementation + +**Preferred**: Config merging via `baseutil.MergeTranslatedConfigs()` - desugared struct is merge parent, user config is child (allows user overrides) + +**Alternative**: Direct struct modification (only if merging isn't expressive enough) + +### Error Handling + +Define in `config/common/errors.go`: +```go +var ErrExample = errors.New("message") +``` + +Use: `r.AddOnError(path.New("json", "field"), common.ErrExample)` + +### Documentation + +After spec changes: +1. Update `internal/doc/butane.yaml` +2. Run `./generate` +3. Update `docs/examples.md` and `docs/release-notes.md` + +## Commit Conventions + +**Format**: `component: description in imperative mood` + +**Component patterns** (NO file extensions): +- `fcos translate: add warn on small partition` +- `fcos translate_test: add tests for detection` +- `docs: run generate` +- `*: update experimental specs` +- `base/v0_7_exp: stabilize to v0_7` + +**Style**: +- Imperative tense ("add" not "added") +- Lowercase component and description +- Under 72 characters +- NO file extensions (.go, .md) + +## Testing + +**ALWAYS run before commit**: `./test` + +Runs: `gofmt -d`, `go vet -composites=false`, `go test ./... -cover`, doc validation + +**Test patterns**: Table-driven tests, positive/negative cases, edge cases, `*_test.go` files alongside code + +## OpenCode Skills + +Located in `.opencode/skills/`: + +- **add-sugar**: `/add-sugar --spec fcos/v1_8_exp` - Scaffolds sugar features (schema, translate, validate, tests, docs) +- **stabilize-spec**: `/stabilize-spec --spec fcos/v1_8_exp` - Stabilizes experimental specs, creates new experimental +- **remove-feature**: `/remove-feature --spec fcos/v1_8_exp --field old_sugar` - Safely removes features + +**Global skills**: `add-skill`, `commit-message`, `review-pr` + +## Automation Scripts + +`.opencode/scripts/`: + +- **version-info.sh**: `eval "$(.opencode/scripts/version-info.sh config/fcos/v1_8_exp)"` - Sets PACKAGE, IS_EXP, DOTTED, SPEC_TYPE vars +- **preflight.sh**: `--exists`, `--experimental`, `--git-clean`, `--all-experimental` - Pre-flight checks + +## Development Workflow + +**Adding Features**: +1. Target experimental spec (`base/v0_8_exp` for distro-independent, `config/fcos/v1_8_exp` for distro-specific) +2. Use `/add-sugar --spec {spec}` or manual: update schema.go → translate.go → validate.go → tests → errors.go → doc/butane.yaml +3. Run `./generate` +4. Run `./test` +5. Commit: `git commit -m "component: description"` + +**Stabilizing**: Use `/stabilize-spec` or follow [stabilization checklist](https://github.com/coreos/butane/issues/new?template=stabilize-checklist.md) + +**Releases**: Follow [release checklist](https://github.com/coreos/butane/issues/new?template=release-checklist.md) + +## Important Rules + +**Translation**: Prefer config merging; desugared = parent, user = child + +**Validation**: All errors in `config/common/errors.go`; use path tracking; distinguish errors (fatal) vs warnings + +**Testing**: Run `./test` before every commit (non-negotiable) + +**Documentation**: Run `./generate` after changes; keep examples and release notes updated + +**Code Quality**: `gofmt` formatting, `golangci-lint` in CI, `go vet -composites=false` + +## Resources + +- Docs: `docs/getting-started.md`, `docs/specs.md`, `docs/development.md` +- Dependencies: Ignition (github.com/coreos/ignition), vcontext (github.com/coreos/vcontext) +- Main branch: `main`, CI: format/vet/tests/linting + +## Quick Reference + +- Read existing code before changes +- Use experimental specs (`*_exp`) for new features +- Use config merging for sugar unless impossible +- Check preflight: `.opencode/scripts/preflight.sh --all-experimental {spec}` +- Butane configs should be intuitive and human-friendly diff --git a/butane/CLAUDE.md b/butane/CLAUDE.md new file mode 100644 index 000000000..31671110f --- /dev/null +++ b/butane/CLAUDE.md @@ -0,0 +1,19 @@ +@AGENTS.md + +## Claude Code Specific Workflows + +This file imports the main repository specification from AGENTS.md and adds Claude Code-specific instructions. + +### Task Management + +When working on multi-step tasks, use `TaskCreate` to break down work and track progress. + +### Agent Usage + +- Use the **Explore** agent for broad codebase searches when simple Grep/Glob isn't enough +- Use **Plan** mode (if available) for complex changes affecting multiple files +- Delegate independent research to subagents to keep main context clean + +### Testing + +Always run `./test` before committing - this is non-negotiable and enforced by the repository. 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..ecc124060 --- /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/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..edb0d9d80 --- /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..ad8c3b875 --- /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/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..559b60222 --- /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/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..0981dfe23 --- /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/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..b7bc03592 --- /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..2deba4d6f --- /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..3531cd17e --- /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/butane/config/common" + "github.com/coreos/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..bafc211bc --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..45f1bcbf0 --- /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/butane/base/util" + "github.com/coreos/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..900b3a720 --- /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/butane/base/util" + "github.com/coreos/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..a6e97bf48 --- /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..9dacb85da --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..3d3cfbe01 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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 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..7612dbc54 --- /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/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..8822315e0 --- /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/butane/base/util" + "github.com/coreos/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..b6b21602d --- /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/butane/base/util" + "github.com/coreos/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..2b9182255 --- /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..7bd03c2f7 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..c0500dd0c --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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 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..fc693edaf --- /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/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..5a761c58f --- /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/butane/base/util" + "github.com/coreos/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..d26c8f347 --- /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/butane/base/util" + "github.com/coreos/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..d1ae21885 --- /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..56cfff203 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..42a0f9d96 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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 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..ff3c36481 --- /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/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..f63d257a1 --- /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/butane/base/util" + "github.com/coreos/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..a92e18285 --- /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/butane/base/util" + "github.com/coreos/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..8804bf7ba --- /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..d214d6080 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..a9f2f151e --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 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..53a027e3e --- /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/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..49de85749 --- /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/butane/base/util" + "github.com/coreos/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..98a7480c2 --- /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/butane/base/util" + "github.com/coreos/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..bb56c3722 --- /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..0b5d306d4 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..0ed14723b --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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 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..4098db276 --- /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/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..88b57ede8 --- /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/butane/base/util" + "github.com/coreos/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..20f014e7f --- /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/butane/base/util" + "github.com/coreos/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..0da3b892d --- /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..0b4142826 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..03a45ce9f --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 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..9899a44d9 --- /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/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..4b9bc3853 --- /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/butane/base/util" + "github.com/coreos/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..f65eaeadd --- /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/butane/base/util" + "github.com/coreos/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..219c8aa99 --- /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..916ee3268 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + "github.com/coreos/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..f87bbba3c --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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 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..35b9b24e4 --- /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/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..6469ad4c7 --- /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/butane/base/util" + "github.com/coreos/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..6e9428655 --- /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/butane/base/util" + "github.com/coreos/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..139644d97 --- /dev/null +++ b/butane/build @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eu + +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" + +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" internal/main.go 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..d23cdc601 --- /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..c22a3c990 --- /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..dc34dbe33 --- /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/butane/config/common" + fcos1_0 "github.com/coreos/butane/config/fcos/v1_0" + fcos1_1 "github.com/coreos/butane/config/fcos/v1_1" + fcos1_2 "github.com/coreos/butane/config/fcos/v1_2" + fcos1_3 "github.com/coreos/butane/config/fcos/v1_3" + fcos1_4 "github.com/coreos/butane/config/fcos/v1_4" + fcos1_5 "github.com/coreos/butane/config/fcos/v1_5" + fcos1_6 "github.com/coreos/butane/config/fcos/v1_6" + fcos1_7 "github.com/coreos/butane/config/fcos/v1_7" + fcos1_8_exp "github.com/coreos/butane/config/fcos/v1_8_exp" + fiot1_0 "github.com/coreos/butane/config/fiot/v1_0" + fiot1_1_exp "github.com/coreos/butane/config/fiot/v1_1_exp" + flatcar1_0 "github.com/coreos/butane/config/flatcar/v1_0" + flatcar1_1 "github.com/coreos/butane/config/flatcar/v1_1" + flatcar1_2_exp "github.com/coreos/butane/config/flatcar/v1_2_exp" + openshift4_10 "github.com/coreos/butane/config/openshift/v4_10" + openshift4_11 "github.com/coreos/butane/config/openshift/v4_11" + openshift4_12 "github.com/coreos/butane/config/openshift/v4_12" + openshift4_13 "github.com/coreos/butane/config/openshift/v4_13" + openshift4_14 "github.com/coreos/butane/config/openshift/v4_14" + openshift4_15 "github.com/coreos/butane/config/openshift/v4_15" + openshift4_16 "github.com/coreos/butane/config/openshift/v4_16" + openshift4_17 "github.com/coreos/butane/config/openshift/v4_17" + openshift4_18 "github.com/coreos/butane/config/openshift/v4_18" + openshift4_19 "github.com/coreos/butane/config/openshift/v4_19" + openshift4_20 "github.com/coreos/butane/config/openshift/v4_20" + openshift4_21 "github.com/coreos/butane/config/openshift/v4_21" + openshift4_22 "github.com/coreos/butane/config/openshift/v4_22" + openshift4_23_exp "github.com/coreos/butane/config/openshift/v4_23_exp" + openshift4_8 "github.com/coreos/butane/config/openshift/v4_8" + openshift4_9 "github.com/coreos/butane/config/openshift/v4_9" + r4e1_0 "github.com/coreos/butane/config/r4e/v1_0" + r4e1_1 "github.com/coreos/butane/config/r4e/v1_1" + r4e1_2_exp "github.com/coreos/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..1c5301fd9 --- /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/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..d8e0ebf62 --- /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/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..efffa976f --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_1" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..6f4bdc531 --- /dev/null +++ b/butane/config/fcos/v1_0/validate_test.go @@ -0,0 +1,113 @@ +// 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/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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..58bf6b82e --- /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/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..1a38c3ed9 --- /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/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..5ba8bc45b --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_2" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..3eefb997c --- /dev/null +++ b/butane/config/fcos/v1_1/validate_test.go @@ -0,0 +1,123 @@ +// 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/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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..42fb94682 --- /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/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..9399728c0 --- /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/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..53e3dbc98 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..7cd5a2e06 --- /dev/null +++ b/butane/config/fcos/v1_2/validate_test.go @@ -0,0 +1,123 @@ +// 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/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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..1fb1cb0fc --- /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/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..c506427dc --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..2e96e4a47 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..ce0eb8b9a --- /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/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..581e0ad18 --- /dev/null +++ b/butane/config/fcos/v1_3/validate_test.go @@ -0,0 +1,192 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..51c4de30d --- /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/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..8efdaf175 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..d835d1ceb --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_4" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..5ad45dde7 --- /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/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..674109335 --- /dev/null +++ b/butane/config/fcos/v1_4/validate_test.go @@ -0,0 +1,192 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_4" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..f00e53c77 --- /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/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..8628029ff --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +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..ba47b0f55 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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..add1056d3 --- /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/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..f83c6db4f --- /dev/null +++ b/butane/config/fcos/v1_5/validate_test.go @@ -0,0 +1,237 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..dd63a0836 --- /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/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..6ed66c7ae --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..cf9cf07cc --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..6dd6f614f --- /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/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..f2e7db0e7 --- /dev/null +++ b/butane/config/fcos/v1_6/validate_test.go @@ -0,0 +1,668 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..1266e6ee0 --- /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/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..870bf5ab8 --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..29e2fd689 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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..e1aa2fcd8 --- /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/butane/base/v0_7" + "github.com/coreos/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +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..4fb483fb2 --- /dev/null +++ b/butane/config/fcos/v1_7/validate_test.go @@ -0,0 +1,651 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..b27c59946 --- /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/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..ceb167a4d --- /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/butane/base/util" + "github.com/coreos/butane/config/common" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..810b3f22b --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..8f927e1be --- /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/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +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..59f5a03fc --- /dev/null +++ b/butane/config/fcos/v1_8_exp/validate_test.go @@ -0,0 +1,651 @@ +// 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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// 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 validation error, partition list + { + `storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..d9c6f0dc3 --- /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/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..529629ed9 --- /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/butane/config/common" + cutil "github.com/coreos/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..77215d9a0 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..e60659cdf --- /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/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..c884709c2 --- /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/butane/config/common" + cutil "github.com/coreos/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..bafd6d4de --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..6a1d7366a --- /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/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..a6a7821ec --- /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/butane/config/common" + cutil "github.com/coreos/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..4533f33d6 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_4" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..f27027370 --- /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/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..8f6f28a03 --- /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/butane/config/common" + cutil "github.com/coreos/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..89bc0c2d0 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..bef41f95e --- /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/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..f219a88e5 --- /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/butane/config/common" + cutil "github.com/coreos/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..086a165df --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..37e49f302 --- /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..6d6e0ce52 --- /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/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..5ab6a7e1c --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_10/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..5332a9c6e --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_10/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..e145103d0 --- /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/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..35c0104c5 --- /dev/null +++ b/butane/config/openshift/v4_10/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..37e49f302 --- /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..eac0a311c --- /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/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..9382e41e8 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_11/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..981f3c5bc --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_11/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..3e78041db --- /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/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..ee2bdb4db --- /dev/null +++ b/butane/config/openshift/v4_11/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..37e49f302 --- /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..e143b5470 --- /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/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..2687288d8 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_12/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..4b778f69e --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_12/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..48205bb96 --- /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/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..a8fe74bac --- /dev/null +++ b/butane/config/openshift/v4_12/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..37e49f302 --- /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..54e6bfda0 --- /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/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..482ea0d73 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_13/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..d198f9653 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_13/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..ba41834f3 --- /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/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..0c3a80d98 --- /dev/null +++ b/butane/config/openshift/v4_13/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..ad5abd8ee --- /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..4ef6cf204 --- /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/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..ae7e36f43 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_14/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_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..4d163de2d --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_14/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_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..d1becc02f --- /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/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..86e132836 --- /dev/null +++ b/butane/config/openshift/v4_14/validate_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_14 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_15/result/schema.go b/butane/config/openshift/v4_15/result/schema.go new file mode 100644 index 000000000..ad5abd8ee --- /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..1c8ca4795 --- /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/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..f1cb32868 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_15/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_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..d0878e398 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_15/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_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..703d90a68 --- /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/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..4aba42487 --- /dev/null +++ b/butane/config/openshift/v4_15/validate_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_15 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_16/result/schema.go b/butane/config/openshift/v4_16/result/schema.go new file mode 100644 index 000000000..ad5abd8ee --- /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..bb58edbee --- /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/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..11e28065a --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_16/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_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..496eb06c5 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_16/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_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..a1ffb7a67 --- /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/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..e90140f87 --- /dev/null +++ b/butane/config/openshift/v4_16/validate_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_16 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_17/result/schema.go b/butane/config/openshift/v4_17/result/schema.go new file mode 100644 index 000000000..ad5abd8ee --- /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..217452533 --- /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/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..6be88245f --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_17/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_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..860399300 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_17/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_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..0fb42c394 --- /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/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..5745a0171 --- /dev/null +++ b/butane/config/openshift/v4_17/validate_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_17 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_18/result/schema.go b/butane/config/openshift/v4_18/result/schema.go new file mode 100644 index 000000000..ad5abd8ee --- /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..c9da45ad3 --- /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/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..b3d78f74d --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_18/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +const ( + // FIPS 140-2 doesn't allow the default XTS mode + fipsCipherOption = types.LuksOption("--cipher") + fipsCipherShortOption = types.LuksOption("-c") + fipsCipherArgument = types.LuksOption("aes-cbc-essiv:sha256") +) + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_18Unvalidated translates the config to a MachineConfig. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToMachineConfig4_18Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_4Unvalidated(options) + if r.IsFatal() { + return result.MachineConfig{}, ts, r + } + + // wrap + ts = ts.PrefixPaths(path.New("yaml"), path.New("json", "spec", "config")) + mc := result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: c.Metadata.Name, + Labels: make(map[string]string), + }, + Spec: result.Spec{ + Config: cfg, + }, + } + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "apiVersion")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "kind")) + ts.AddTranslation(path.New("yaml", "metadata"), path.New("json", "metadata")) + ts.AddTranslation(path.New("yaml", "metadata", "name"), path.New("json", "metadata", "name")) + ts.AddTranslation(path.New("yaml", "metadata", "labels"), path.New("json", "metadata", "labels")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "spec")) + ts.AddTranslation(path.New("yaml"), path.New("json", "spec", "config")) + for k, v := range c.Metadata.Labels { + mc.Metadata.Labels[k] = v + ts.AddTranslation(path.New("yaml", "metadata", "labels", k), path.New("json", "metadata", "labels", k)) + } + + // translate OpenShift fields + tr := translate.NewTranslator("yaml", "json", options) + from := &c.OpenShift + to := &mc.Spec + ts2, r2 := translate.Prefixed(tr, "extensions", &from.Extensions, &to.Extensions) + translate.MergeP(tr, ts2, &r2, "fips", &from.FIPS, &to.FIPS) + translate.MergeP2(tr, ts2, &r2, "kernel_arguments", &from.KernelArguments, "kernelArguments", &to.KernelArguments) + translate.MergeP2(tr, ts2, &r2, "kernel_type", &from.KernelType, "kernelType", &to.KernelType) + ts.MergeP2("openshift", "spec", ts2) + r.Merge(r2) + + // apply FIPS options to LUKS volumes + ts.Merge(addLuksFipsOptions(&mc)) + + // finally, check the fully desugared config for RHCOS and MCO support + r.Merge(validateRHCOSSupport(mc)) + r.Merge(validateMCOSupport(mc)) + + return mc, ts, r +} + +// ToMachineConfig4_18 translates the config to a MachineConfig. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToMachineConfig4_18(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_18Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_4Unvalidated translates the config to an Ignition config. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToIgn3_4Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_18Unvalidated(options) + cfg := mc.Spec.Config + + // report warnings if there are any non-empty fields in Spec (other + // than the Ignition config itself) that we're ignoring + mc.Spec.Config = types.Config{} + warnings := translate.PrefixReport(cutil.CheckForElidedFields(mc.Spec), "spec") + // translate from json space into yaml space, since the caller won't + // have enough info to do it + r.Merge(cutil.TranslateReportPaths(warnings, ts)) + + ts = ts.Descend(path.New("json", "spec", "config")) + return cfg, ts, r +} + +// ToIgn3_4 translates the config to an Ignition config. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToIgn3_4(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_4Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.17 Butane config to a v4.17 MachineConfig or a v3.6.0-experimental Ignition config. It returns a report of any errors or +// warnings in the source and resultant config. If the report has fatal errors or it encounters other problems +// translating, an error is returned. +func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + if options.Raw { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_4", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_18", options) + } +} + +func addLuksFipsOptions(mc *result.MachineConfig) translate.TranslationSet { + ts := translate.NewTranslationSet("yaml", "json") + if !util.IsTrue(mc.Spec.FIPS) { + return ts + } + +OUTER: + for i := range mc.Spec.Config.Storage.Luks { + luks := &mc.Spec.Config.Storage.Luks[i] + // Only add options if the user hasn't already specified + // a cipher option. Do this in-place, since config merging + // doesn't support conditional logic. + for _, option := range luks.Options { + if option == fipsCipherOption || + strings.HasPrefix(string(option), string(fipsCipherOption)+"=") || + option == fipsCipherShortOption { + continue OUTER + } + } + for j := 0; j < 2; j++ { + ts.AddTranslation(path.New("yaml", "openshift", "fips"), path.New("json", "spec", "config", "storage", "luks", i, "options", len(luks.Options)+j)) + } + if len(luks.Options) == 0 { + ts.AddTranslation(path.New("yaml", "openshift", "fips"), path.New("json", "spec", "config", "storage", "luks", i, "options")) + } + luks.Options = append(luks.Options, fipsCipherOption, fipsCipherArgument) + } + return ts +} + +// Error on fields that are rejected by RHCOS. +// +// Some of these fields may have been generated by sugar (e.g. +// boot_device.luks), so we work in JSON (output) space and then translate +// paths back to YAML (input) space. That's also the reason we do these +// checks after translation, rather than during validation. +func validateRHCOSSupport(mc result.MachineConfig) report.Report { + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "btrfs" { + // we don't ship mkfs.btrfs + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrBtrfsSupport) + } + } + return r +} + +// Error on fields that are rejected outright by the MCO, or that are +// unsupported by the MCO and we want to discourage. +// +// https://github.com/openshift/machine-config-operator/blob/d6dabadeca05/MachineConfigDaemon.md#supported-vs-unsupported-ignition-config-changes +// +// Some of these fields may have been generated by sugar (e.g. storage.trees), +// so we work in JSON (output) space and then translate paths back to YAML +// (input) space. That's also the reason we do these checks after +// translation, rather than during validation. +func validateMCOSupport(mc result.MachineConfig) report.Report { + // See also fieldFilters at the top of this file. + + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "none" { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrFilesystemNoneSupport) + } + } + for i, file := range mc.Spec.Config.Storage.Files { + if file.Contents.Source != nil { + fileSource, err := url.Parse(*file.Contents.Source) + // parse errors will be caught by normal config validation + if err == nil && fileSource.Scheme != "data" { + // FORBIDDEN + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "contents", "source"), common.ErrFileSchemeSupport) + } + } + if file.Mode != nil && *file.Mode & ^0777 != 0 { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "mode"), common.ErrFileSpecialModeSupport) + } + } + for i, user := range mc.Spec.Config.Passwd.Users { + if user.Name != "core" { + // TRIPWIRE + r.AddOnError(path.New("json", "spec", "config", "passwd", "users", i, "name"), common.ErrUserNameSupport) + } + } + return r +} diff --git a/butane/config/openshift/v4_18/translate_test.go b/butane/config/openshift/v4_18/translate_test.go new file mode 100644 index 000000000..117f8965b --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_5" + "github.com/coreos/butane/config/openshift/v4_18/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_4Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + // FIPS + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + OpenShift: OpenShift{ + FIPS: util.BoolToPtr(true), + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Luks: []base.Luks{ + { + Name: "a", + }, + { + Name: "b", + Options: []string{"b", "b"}, + }, + { + Name: "c", + Options: []string{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []string{"--cipher=z"}, + }, + { + Name: "e", + Options: []string{"-c", "z"}, + }, + { + Name: "f", + Options: []string{"--ciphertext"}, + }, + }, + }, + }, + BootDevice: fcos.BootDevice{ + Luks: fcos.BootDeviceLuks{ + Tpm2: util.BoolToPtr(true), + }, + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.4.0", + }, + Storage: types.Storage{ + Filesystems: []types.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []types.Luks{ + { + Name: "root", + Device: util.StrToPtr("/dev/disk/by-partlabel/root"), + Label: util.StrToPtr("luks-root"), + WipeVolume: util.BoolToPtr(true), + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + Clevis: types.Clevis{ + Tpm2: util.BoolToPtr(true), + }, + }, + { + Name: "a", + Options: []types.LuksOption{fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "b", + Options: []types.LuksOption{"b", "b", fipsCipherOption, fipsCipherArgument}, + }, + { + Name: "c", + Options: []types.LuksOption{"c", "--cipher", "c"}, + }, + { + Name: "d", + Options: []types.LuksOption{"--cipher=z"}, + }, + { + Name: "e", + Options: []types.LuksOption{"-c", "z"}, + }, + { + Name: "f", + Options: []types.LuksOption{"--ciphertext", fipsCipherOption, fipsCipherArgument}, + }, + }, + }, + }, + FIPS: util.BoolToPtr(true), + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + {From: path.New("yaml", "boot_device", "luks", "tpm2"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis", "tpm2")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "clevis")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "device")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "label")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "name")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0, "wipeVolume")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 0, "options")}, + {From: path.New("yaml", "boot_device", "luks"), To: path.New("json", "spec", "config", "storage", "luks", 0)}, + {From: path.New("yaml", "storage", "luks", 0, "name"), To: path.New("json", "spec", "config", "storage", "luks", 1, "name")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 1, "options")}, + {From: path.New("yaml", "storage", "luks", 0), To: path.New("json", "spec", "config", "storage", "luks", 1)}, + {From: path.New("yaml", "storage", "luks", 1, "name"), To: path.New("json", "spec", "config", "storage", "luks", 2, "name")}, + {From: path.New("yaml", "storage", "luks", 1, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 1, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 2)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options", 3)}, + {From: path.New("yaml", "storage", "luks", 1, "options"), To: path.New("json", "spec", "config", "storage", "luks", 2, "options")}, + {From: path.New("yaml", "storage", "luks", 1), To: path.New("json", "spec", "config", "storage", "luks", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "name"), To: path.New("json", "spec", "config", "storage", "luks", 3, "name")}, + {From: path.New("yaml", "storage", "luks", 2, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 2, "options", 2), To: path.New("json", "spec", "config", "storage", "luks", 3, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 2, "options"), To: path.New("json", "spec", "config", "storage", "luks", 3, "options")}, + {From: path.New("yaml", "storage", "luks", 2), To: path.New("json", "spec", "config", "storage", "luks", 3)}, + {From: path.New("yaml", "storage", "luks", 3, "name"), To: path.New("json", "spec", "config", "storage", "luks", 4, "name")}, + {From: path.New("yaml", "storage", "luks", 3, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 4, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 3, "options"), To: path.New("json", "spec", "config", "storage", "luks", 4, "options")}, + {From: path.New("yaml", "storage", "luks", 3), To: path.New("json", "spec", "config", "storage", "luks", 4)}, + {From: path.New("yaml", "storage", "luks", 4, "name"), To: path.New("json", "spec", "config", "storage", "luks", 5, "name")}, + {From: path.New("yaml", "storage", "luks", 4, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 0)}, + {From: path.New("yaml", "storage", "luks", 4, "options", 1), To: path.New("json", "spec", "config", "storage", "luks", 5, "options", 1)}, + {From: path.New("yaml", "storage", "luks", 4, "options"), To: path.New("json", "spec", "config", "storage", "luks", 5, "options")}, + {From: path.New("yaml", "storage", "luks", 4), To: path.New("json", "spec", "config", "storage", "luks", 5)}, + {From: path.New("yaml", "storage", "luks", 5, "name"), To: path.New("json", "spec", "config", "storage", "luks", 6, "name")}, + {From: path.New("yaml", "storage", "luks", 5, "options", 0), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 0)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 1)}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options", 2)}, + {From: path.New("yaml", "storage", "luks", 5, "options"), To: path.New("json", "spec", "config", "storage", "luks", 6, "options")}, + {From: path.New("yaml", "storage", "luks", 5), To: path.New("json", "spec", "config", "storage", "luks", 6)}, + {From: path.New("yaml", "storage", "luks"), To: path.New("json", "spec", "config", "storage", "luks")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "device")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "format")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "label")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0, "wipeFilesystem")}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems", 0)}, + {From: path.New("yaml", "boot_device"), To: path.New("json", "spec", "config", "storage", "filesystems")}, + {From: path.New("yaml", "storage"), To: path.New("json", "spec", "config", "storage")}, + {From: path.New("yaml", "openshift", "fips"), To: path.New("json", "spec", "fips")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// Test post-translation validation of RHCOS/MCO support for Ignition config fields. +func TestValidateSupport(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + []entry{}, + }, + // core user with only accepted fields + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + PasswordHash: util.StrToPtr("corned beef"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // valid data URL + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + Contents: base.Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // all the warnings/errors + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + }, + { + Path: "/g", + Append: []base.Resource{ + { + Inline: util.StrToPtr("z"), + }, + }, + }, + { + Path: "/h", + Contents: base.Resource{ + Source: util.StrToPtr("https://example.com/"), + }, + Mode: util.IntToPtr(04755), + }, + { + Path: "/i", + Contents: base.Resource{ + Source: util.StrToPtr("data:,z"), + HTTPHeaders: base.HTTPHeaders{ + { + Name: "foo", + Value: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/vda4", + Format: util.StrToPtr("btrfs"), + }, + { + Device: "/dev/vda5", + Format: util.StrToPtr("none"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: util.StrToPtr("/t"), + }, + }, + }, + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + Gecos: util.StrToPtr("mercury delay line"), + Groups: []base.Group{ + "z", + }, + HomeDir: util.StrToPtr("/home/drum"), + NoCreateHome: util.BoolToPtr(true), + NoLogInit: util.BoolToPtr(true), + NoUserGroup: util.BoolToPtr(true), + PasswordHash: util.StrToPtr("corned beef"), + PrimaryGroup: util.StrToPtr("wheel"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + SSHAuthorizedKeysLocal: []string{}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "foo", + }, + ShouldNotExist: []base.KernelArgument{ + "bar", + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "format")}, + {report.Error, common.ErrFilesystemNoneSupport, path.New("yaml", "storage", "filesystems", 1, "format")}, + {report.Error, common.ErrFileSchemeSupport, path.New("yaml", "storage", "files", 2, "contents", "source")}, + {report.Error, common.ErrFileSpecialModeSupport, path.New("yaml", "storage", "files", 2, "mode")}, + {report.Error, common.ErrUserNameSupport, path.New("yaml", "passwd", "users", 1, "name")}, + // filters + {report.Error, common.ErrKernelArgumentSupport, path.New("yaml", "kernel_arguments")}, + {report.Error, common.ErrGroupSupport, path.New("yaml", "passwd", "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "gecos")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "home_dir")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_create_home")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_log_init")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_user_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "primary_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "shell")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "should_exist")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "system")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "uid")}, + {report.Error, common.ErrDirectorySupport, path.New("yaml", "storage", "directories")}, + {report.Error, common.ErrFileAppendSupport, path.New("yaml", "storage", "files", 1, "append")}, + {report.Error, common.ErrFileHeaderSupport, path.New("yaml", "storage", "files", 3, "contents", "http_headers")}, + {report.Error, common.ErrLinkSupport, path.New("yaml", "storage", "links")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + var expectedReport report.Report + for _, entry := range test.entries { + expectedReport.AddOn(entry.path, entry.err, entry.kind) + } + actual, translations, r := test.in.ToMachineConfig4_18Unvalidated(common.TranslateOptions{}) + r.Merge(fieldFilters.Verify(actual)) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expectedReport, r, "report mismatch") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/openshift/v4_18/validate.go b/butane/config/openshift/v4_18/validate.go new file mode 100644 index 000000000..a45ec6060 --- /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/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..b6f9170e5 --- /dev/null +++ b/butane/config/openshift/v4_18/validate_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_18 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_19/result/schema.go b/butane/config/openshift/v4_19/result/schema.go new file mode 100644 index 000000000..62bc2054e --- /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..6154a0a3e --- /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/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..ac0807b08 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_19/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..9117d8243 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_6" + "github.com/coreos/butane/config/openshift/v4_19/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..89de23ba6 --- /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/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..8a9887557 --- /dev/null +++ b/butane/config/openshift/v4_19/validate_test.go @@ -0,0 +1,388 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_19 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/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 validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_20/result/schema.go b/butane/config/openshift/v4_20/result/schema.go new file mode 100644 index 000000000..62bc2054e --- /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..180aca67f --- /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/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..0da25dc47 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_20/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/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..dfb800e74 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_6" + "github.com/coreos/butane/config/openshift/v4_20/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..3a1a1925b --- /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/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..fdf534081 --- /dev/null +++ b/butane/config/openshift/v4_20/validate_test.go @@ -0,0 +1,388 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_20 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/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 validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_21/result/schema.go b/butane/config/openshift/v4_21/result/schema.go new file mode 100644 index 000000000..62bc2054e --- /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..4540d0510 --- /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/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..1c41dabae --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_21/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/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..b2985849c --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_6" + "github.com/coreos/butane/config/openshift/v4_21/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..33a7db8ff --- /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/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..a3b44dd9c --- /dev/null +++ b/butane/config/openshift/v4_21/validate_test.go @@ -0,0 +1,388 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_21 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_6" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/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 validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_22/result/schema.go b/butane/config/openshift/v4_22/result/schema.go new file mode 100644 index 000000000..c38ee02e7 --- /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_6/types" +) + +const ( + MC_API_VERSION = "machineconfiguration.openshift.io/v1" + MC_KIND = "MachineConfig" +) + +// We round-trip through JSON because Ignition uses `json` struct tags, +// so all struct tags need to be `json` even though we're ultimately +// writing YAML. + +type MachineConfig struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` +} + +type Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels,omitempty"` +} + +type Spec struct { + Config types.Config `json:"config"` + KernelArguments []string `json:"kernelArguments,omitempty"` + Extensions []string `json:"extensions,omitempty"` + FIPS *bool `json:"fips,omitempty"` + KernelType *string `json:"kernelType,omitempty"` +} diff --git a/butane/config/openshift/v4_22/schema.go b/butane/config/openshift/v4_22/schema.go new file mode 100644 index 000000000..8f76b49e0 --- /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/butane/config/fcos/v1_7" +) + +const ROLE_LABEL_KEY = "machineconfiguration.openshift.io/role" + +type Config struct { + fcos.Config `yaml:",inline"` + Metadata Metadata `yaml:"metadata"` + OpenShift OpenShift `yaml:"openshift"` +} + +type Metadata struct { + Name string `yaml:"name"` + Labels map[string]string `yaml:"labels,omitempty"` +} + +type OpenShift struct { + KernelArguments []string `yaml:"kernel_arguments"` + Extensions []string `yaml:"extensions"` + FIPS *bool `yaml:"fips"` + KernelType *string `yaml:"kernel_type"` +} diff --git a/butane/config/openshift/v4_22/translate.go b/butane/config/openshift/v4_22/translate.go new file mode 100644 index 000000000..f4b76841d --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_22/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// Error classes: +// +// UNPARSABLE - Cannot be rendered into a config by the MCC. If present in +// MC, MCC will mark the pool degraded. We reject these. +// +// FORBIDDEN - Not supported by the MCD. If present in MC, MCD will mark +// the node degraded. We reject these. +// +// REDUNDANT - Feature is also provided by a MachineConfig-specific field +// with different semantics. To reduce confusion, disable this +// implementation. +// +// IMMUTABLE - Permitted in MC, passed through to Ignition, but not +// supported by the MCD. MCD will mark the node degraded if the field +// changes after the node is provisioned. We reject these outright to +// discourage their use. +// +// TRIPWIRE - A subset of fields in the containing struct are supported by +// the MCD. If the struct contents change after the node is provisioned, +// and the struct contains unsupported fields, MCD will mark the node +// degraded, even if the change only affects supported fields. We reject +// these. + +var ( + // See also validateRHCOSSupport() and validateMCOSupport() + fieldFilters = cutil.NewFilters(result.MachineConfig{}, cutil.FilterMap{ + // UNPARSABLE, REDUNDANT + "spec.config.kernelArguments": common.ErrKernelArgumentSupport, + // IMMUTABLE + "spec.config.passwd.groups": common.ErrGroupSupport, + // TRIPWIRE + "spec.config.passwd.users.gecos": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.groups": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.homeDir": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noCreateHome": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noLogInit": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.noUserGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.primaryGroup": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shell": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.shouldExist": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.system": common.ErrUserFieldSupport, + // TRIPWIRE + "spec.config.passwd.users.uid": common.ErrUserFieldSupport, + // IMMUTABLE + "spec.config.storage.directories": common.ErrDirectorySupport, + // FORBIDDEN + "spec.config.storage.files.append": common.ErrFileAppendSupport, + // redundant with a check from Ignition validation, but ensures we + // exclude the section from docs + "spec.config.storage.files.contents.httpHeaders": common.ErrFileHeaderSupport, + // IMMUTABLE + // If you change this to be less restrictive without adding + // link support in the MCO, consider what should happen if + // the user specifies a storage.tree that includes symlinks. + "spec.config.storage.links": common.ErrLinkSupport, + }) +) + +// Return FieldFilters for this spec. +func (c Config) FieldFilters() *cutil.FieldFilters { + return &fieldFilters +} + +// ToMachineConfig4_22Unvalidated translates the config to a MachineConfig. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToMachineConfig4_22Unvalidated(options common.TranslateOptions) (result.MachineConfig, translate.TranslationSet, report.Report) { + cfg, ts, r := c.Config.ToIgn3_6Unvalidated(options) + if r.IsFatal() { + return result.MachineConfig{}, ts, r + } + + // wrap + ts = ts.PrefixPaths(path.New("yaml"), path.New("json", "spec", "config")) + mc := result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: c.Metadata.Name, + Labels: make(map[string]string), + }, + Spec: result.Spec{ + Config: cfg, + }, + } + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "apiVersion")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "kind")) + ts.AddTranslation(path.New("yaml", "metadata"), path.New("json", "metadata")) + ts.AddTranslation(path.New("yaml", "metadata", "name"), path.New("json", "metadata", "name")) + ts.AddTranslation(path.New("yaml", "metadata", "labels"), path.New("json", "metadata", "labels")) + ts.AddTranslation(path.New("yaml", "version"), path.New("json", "spec")) + ts.AddTranslation(path.New("yaml"), path.New("json", "spec", "config")) + for k, v := range c.Metadata.Labels { + mc.Metadata.Labels[k] = v + ts.AddTranslation(path.New("yaml", "metadata", "labels", k), path.New("json", "metadata", "labels", k)) + } + + // translate OpenShift fields + tr := translate.NewTranslator("yaml", "json", options) + from := &c.OpenShift + to := &mc.Spec + ts2, r2 := translate.Prefixed(tr, "extensions", &from.Extensions, &to.Extensions) + translate.MergeP(tr, ts2, &r2, "fips", &from.FIPS, &to.FIPS) + translate.MergeP2(tr, ts2, &r2, "kernel_arguments", &from.KernelArguments, "kernelArguments", &to.KernelArguments) + translate.MergeP2(tr, ts2, &r2, "kernel_type", &from.KernelType, "kernelType", &to.KernelType) + ts.MergeP2("openshift", "spec", ts2) + r.Merge(r2) + + // finally, check the fully desugared config for RHCOS and MCO support + r.Merge(validateRHCOSSupport(mc)) + r.Merge(validateMCOSupport(mc)) + + return mc, ts, r +} + +// ToMachineConfig4_22 translates the config to a MachineConfig. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToMachineConfig4_22(options common.TranslateOptions) (result.MachineConfig, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToMachineConfig4_22Unvalidated", options) + return cfg.(result.MachineConfig), r, err +} + +// ToIgn3_6Unvalidated translates the config to an Ignition config. It also +// returns the set of translations it did so paths in the resultant config +// can be tracked back to their source in the source config. No config +// validation is performed on input or output. +func (c Config) ToIgn3_6Unvalidated(options common.TranslateOptions) (types.Config, translate.TranslationSet, report.Report) { + mc, ts, r := c.ToMachineConfig4_22Unvalidated(options) + cfg := mc.Spec.Config + + // report warnings if there are any non-empty fields in Spec (other + // than the Ignition config itself) that we're ignoring + mc.Spec.Config = types.Config{} + warnings := translate.PrefixReport(cutil.CheckForElidedFields(mc.Spec), "spec") + // translate from json space into yaml space, since the caller won't + // have enough info to do it + r.Merge(cutil.TranslateReportPaths(warnings, ts)) + + ts = ts.Descend(path.New("json", "spec", "config")) + return cfg, ts, r +} + +// ToIgn3_6 translates the config to an Ignition config. It returns a +// report of any errors or warnings in the source and resultant config. If +// the report has fatal errors or it encounters other problems translating, +// an error is returned. +func (c Config) ToIgn3_6(options common.TranslateOptions) (types.Config, report.Report, error) { + cfg, r, err := cutil.Translate(c, "ToIgn3_6Unvalidated", options) + return cfg.(types.Config), r, err +} + +// ToConfigBytes translates from a v4.22 Butane config to a v4.22 MachineConfig or a v3.6.0 Ignition config. It returns a report of any errors or +// warnings in the source and resultant config. If the report has fatal errors or it encounters other problems +// translating, an error is returned. +func ToConfigBytes(input []byte, options common.TranslateBytesOptions) ([]byte, report.Report, error) { + if options.Raw { + return cutil.TranslateBytes(input, &Config{}, "ToIgn3_6", options) + } else { + return cutil.TranslateBytesYAML(input, &Config{}, "ToMachineConfig4_22", options) + } +} + +// Error on fields that are rejected by RHCOS. +// +// Some of these fields may have been generated by sugar (e.g. +// boot_device.luks), so we work in JSON (output) space and then translate +// paths back to YAML (input) space. That's also the reason we do these +// checks after translation, rather than during validation. +func validateRHCOSSupport(mc result.MachineConfig) report.Report { + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "btrfs" { + // we don't ship mkfs.btrfs + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrBtrfsSupport) + } + } + return r +} + +// Error on fields that are rejected outright by the MCO, or that are +// unsupported by the MCO and we want to discourage. +// +// https://github.com/openshift/machine-config-operator/blob/d6dabadeca05/MachineConfigDaemon.md#supported-vs-unsupported-ignition-config-changes +// +// Some of these fields may have been generated by sugar (e.g. storage.trees), +// so we work in JSON (output) space and then translate paths back to YAML +// (input) space. That's also the reason we do these checks after +// translation, rather than during validation. +func validateMCOSupport(mc result.MachineConfig) report.Report { + // See also fieldFilters at the top of this file. + + var r report.Report + for i, fs := range mc.Spec.Config.Storage.Filesystems { + if fs.Format != nil && *fs.Format == "none" { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "filesystems", i, "format"), common.ErrFilesystemNoneSupport) + } + } + for i, file := range mc.Spec.Config.Storage.Files { + if file.Contents.Source != nil { + fileSource, err := url.Parse(*file.Contents.Source) + // parse errors will be caught by normal config validation + if err == nil && fileSource.Scheme != "data" { + // FORBIDDEN + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "contents", "source"), common.ErrFileSchemeSupport) + } + } + if file.Mode != nil && *file.Mode & ^0777 != 0 { + // UNPARSABLE + r.AddOnError(path.New("json", "spec", "config", "storage", "files", i, "mode"), common.ErrFileSpecialModeSupport) + } + } + for i, user := range mc.Spec.Config.Passwd.Users { + if user.Name != "core" { + // TRIPWIRE + r.AddOnError(path.New("json", "spec", "config", "passwd", "users", i, "name"), common.ErrUserNameSupport) + } + } + return r +} diff --git a/butane/config/openshift/v4_22/translate_test.go b/butane/config/openshift/v4_22/translate_test.go new file mode 100644 index 000000000..9527cf0eb --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_7" + "github.com/coreos/butane/config/openshift/v4_22/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_6/types" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +// TestElidedFieldWarning tests that we warn when transpiling fields to an +// Ignition config that can't be represented in an Ignition config. +func TestElidedFieldWarning(t *testing.T) { + in := Config{ + Metadata: Metadata{ + Name: "z", + }, + OpenShift: OpenShift{ + KernelArguments: []string{"a", "b"}, + FIPS: util.BoolToPtr(true), + KernelType: util.StrToPtr("realtime"), + }, + } + + var expected report.Report + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_arguments"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "fips"), common.ErrFieldElided) + expected.AddOnWarn(path.New("yaml", "openshift", "kernel_type"), common.ErrFieldElided) + + _, _, r := in.ToIgn3_6Unvalidated(common.TranslateOptions{}) + assert.Equal(t, expected, r, "report mismatch") +} + +func TestTranslateConfig(t *testing.T) { + tests := []struct { + in Config + out result.MachineConfig + exceptions []translate.Translation + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + result.MachineConfig{ + ApiVersion: result.MC_API_VERSION, + Kind: result.MC_KIND, + Metadata: result.Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Spec: result.Spec{ + Config: types.Config{ + Ignition: types.Ignition{ + Version: "3.6.0", + }, + }, + }, + }, + []translate.Translation{ + {From: path.New("yaml", "version"), To: path.New("json", "apiVersion")}, + {From: path.New("yaml", "version"), To: path.New("json", "kind")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec")}, + {From: path.New("yaml"), To: path.New("json", "spec", "config")}, + {From: path.New("yaml", "ignition"), To: path.New("json", "spec", "config", "ignition")}, + {From: path.New("yaml", "version"), To: path.New("json", "spec", "config", "ignition", "version")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + actual, translations, r := test.in.ToMachineConfig4_22Unvalidated(common.TranslateOptions{}) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, test.out, actual, "translation mismatch") + assert.Equal(t, report.Report{}, r, "non-empty report") + baseutil.VerifyTranslations(t, translations, test.exceptions) + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} + +// Test post-translation validation of RHCOS/MCO support for Ignition config fields. +func TestValidateSupport(t *testing.T) { + type entry struct { + kind report.EntryKind + err error + path path.ContextPath + } + tests := []struct { + in Config + entries []entry + }{ + // empty-ish config + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + }, + []entry{}, + }, + // core user with only accepted fields + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + PasswordHash: util.StrToPtr("corned beef"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // valid data URL + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + Contents: base.Resource{ + Source: util.StrToPtr("data:,foo"), + }, + }, + }, + }, + }, + }, + }, + []entry{}, + }, + // all the warnings/errors + { + Config{ + Metadata: Metadata{ + Name: "z", + Labels: map[string]string{ + ROLE_LABEL_KEY: "z", + }, + }, + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Files: []base.File{ + { + Path: "/f", + }, + { + Path: "/g", + Append: []base.Resource{ + { + Inline: util.StrToPtr("z"), + }, + }, + }, + { + Path: "/h", + Contents: base.Resource{ + Source: util.StrToPtr("https://example.com/"), + }, + Mode: util.IntToPtr(04755), + }, + { + Path: "/i", + Contents: base.Resource{ + Source: util.StrToPtr("data:,z"), + HTTPHeaders: base.HTTPHeaders{ + { + Name: "foo", + Value: util.StrToPtr("bar"), + }, + }, + }, + }, + }, + Filesystems: []base.Filesystem{ + { + Device: "/dev/vda4", + Format: util.StrToPtr("btrfs"), + }, + { + Device: "/dev/vda5", + Format: util.StrToPtr("none"), + }, + }, + Directories: []base.Directory{ + { + Path: "/d", + }, + }, + Links: []base.Link{ + { + Path: "/l", + Target: util.StrToPtr("/t"), + }, + }, + }, + Passwd: base.Passwd{ + Users: []base.PasswdUser{ + { + Name: "core", + Gecos: util.StrToPtr("mercury delay line"), + Groups: []base.Group{ + "z", + }, + HomeDir: util.StrToPtr("/home/drum"), + NoCreateHome: util.BoolToPtr(true), + NoLogInit: util.BoolToPtr(true), + NoUserGroup: util.BoolToPtr(true), + PasswordHash: util.StrToPtr("corned beef"), + PrimaryGroup: util.StrToPtr("wheel"), + SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"}, + SSHAuthorizedKeysLocal: []string{}, + Shell: util.StrToPtr("/bin/tcsh"), + ShouldExist: util.BoolToPtr(false), + System: util.BoolToPtr(true), + UID: util.IntToPtr(42), + }, + { + Name: "bovik", + }, + }, + Groups: []base.PasswdGroup{ + { + Name: "mock", + }, + }, + }, + KernelArguments: base.KernelArguments{ + ShouldExist: []base.KernelArgument{ + "foo", + }, + ShouldNotExist: []base.KernelArgument{ + "bar", + }, + }, + }, + }, + }, + []entry{ + // code + {report.Error, common.ErrBtrfsSupport, path.New("yaml", "storage", "filesystems", 0, "format")}, + {report.Error, common.ErrFilesystemNoneSupport, path.New("yaml", "storage", "filesystems", 1, "format")}, + {report.Error, common.ErrFileSchemeSupport, path.New("yaml", "storage", "files", 2, "contents", "source")}, + {report.Error, common.ErrFileSpecialModeSupport, path.New("yaml", "storage", "files", 2, "mode")}, + {report.Error, common.ErrUserNameSupport, path.New("yaml", "passwd", "users", 1, "name")}, + // filters + {report.Error, common.ErrKernelArgumentSupport, path.New("yaml", "kernel_arguments")}, + {report.Error, common.ErrGroupSupport, path.New("yaml", "passwd", "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "gecos")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "groups")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "home_dir")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_create_home")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_log_init")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "no_user_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "primary_group")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "shell")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "should_exist")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "system")}, + {report.Error, common.ErrUserFieldSupport, path.New("yaml", "passwd", "users", 0, "uid")}, + {report.Error, common.ErrDirectorySupport, path.New("yaml", "storage", "directories")}, + {report.Error, common.ErrFileAppendSupport, path.New("yaml", "storage", "files", 1, "append")}, + {report.Error, common.ErrFileHeaderSupport, path.New("yaml", "storage", "files", 3, "contents", "http_headers")}, + {report.Error, common.ErrLinkSupport, path.New("yaml", "storage", "links")}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("translate %d", i), func(t *testing.T) { + var expectedReport report.Report + for _, entry := range test.entries { + expectedReport.AddOn(entry.path, entry.err, entry.kind) + } + actual, translations, r := test.in.ToMachineConfig4_22Unvalidated(common.TranslateOptions{}) + r.Merge(fieldFilters.Verify(actual)) + r = confutil.TranslateReportPaths(r, translations) + baseutil.VerifyReport(t, test.in, r) + assert.Equal(t, expectedReport, r, "report mismatch") + assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage") + }) + } +} diff --git a/butane/config/openshift/v4_22/validate.go b/butane/config/openshift/v4_22/validate.go new file mode 100644 index 000000000..2de8b2aad --- /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/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..33fd75c94 --- /dev/null +++ b/butane/config/openshift/v4_22/validate_test.go @@ -0,0 +1,388 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_22 + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_7" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_7" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // missing kargs for CEX support + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-virt"), + Luks: fcos.BootDeviceLuks{ + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + { + Device: "/dev/mapper/foo", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("foo"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []base.Luks{ + { + Name: "root", + Label: util.StrToPtr("luks-root"), + Device: util.StrToPtr("/dev/disk/by-label/root"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + { + Name: "foo", + Label: util.StrToPtr("luks-foo"), + Device: util.StrToPtr("/dev/disk/by-label/foo"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/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..91f506919 --- /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..aac39e80b --- /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/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..f76aefb07 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_23_exp/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/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..e7be1d067 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_8_exp" + "github.com/coreos/butane/config/openshift/v4_23_exp/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..0a081ee16 --- /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/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..23cac68b1 --- /dev/null +++ b/butane/config/openshift/v4_23_exp/validate_test.go @@ -0,0 +1,388 @@ +// Copyright 2021 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.) + +package v4_23_exp + +import ( + "fmt" + "testing" + + baseutil "github.com/coreos/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_8_exp" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateConfig(t *testing.T) { + tests := []struct { + in Config + out error + errPath path.ContextPath + }{ + // missing kargs for CEX support + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-eckd"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/dasda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-zfcp"), + Luks: fcos.BootDeviceLuks{ + Device: util.StrToPtr("/dev/sda"), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + BootDevice: fcos.BootDevice{ + Layout: util.StrToPtr("s390x-virt"), + Luks: fcos.BootDeviceLuks{ + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + { + Config{ + Config: fcos.Config{ + Config: base.Config{ + Storage: base.Storage{ + Filesystems: []base.Filesystem{ + { + Device: "/dev/mapper/root", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("root"), + WipeFilesystem: util.BoolToPtr(true), + }, + { + Device: "/dev/mapper/foo", + Format: util.StrToPtr("xfs"), + Label: util.StrToPtr("foo"), + WipeFilesystem: util.BoolToPtr(true), + }, + }, + Luks: []base.Luks{ + { + Name: "root", + Label: util.StrToPtr("luks-root"), + Device: util.StrToPtr("/dev/disk/by-label/root"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + { + Name: "foo", + Label: util.StrToPtr("luks-foo"), + Device: util.StrToPtr("/dev/disk/by-label/foo"), + WipeVolume: util.BoolToPtr(true), + Cex: base.Cex{ + Enabled: util.BoolToPtr(true), + }, + }, + }, + }, + }, + }, + OpenShift: OpenShift{ + // explicitly empty kernel argument list + KernelArguments: []string{}, + }, + }, + common.ErrMissingKernelArgumentCex, + path.New("yaml", "openshift", "kernel_arguments"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 11, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + wipe_table: true + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.Error(), + 11, + }, + // Ignition duplicate key check, paths + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + - path: /z`, + errors.ErrDuplicate.Error(), + 9, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + for _, raw := range []bool{false, true} { + _, r, _ := ToConfigBytes([]byte(test.in), common.TranslateBytesOptions{ + Raw: raw, + }) + assert.Len(t, r.Entries, 1, "unexpected report length, raw %v", raw) + assert.Equal(t, test.message, r.Entries[0].Message, "bad error, raw %v", raw) + assert.NotNil(t, r.Entries[0].Marker.StartP, "marker start is nil, raw %v", raw) + assert.Equal(t, test.line, r.Entries[0].Marker.StartP.Line, "incorrect error line, raw %v", raw) + } + }) + } +} diff --git a/butane/config/openshift/v4_8/result/schema.go b/butane/config/openshift/v4_8/result/schema.go new file mode 100644 index 000000000..37e49f302 --- /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..13551d8cf --- /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/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..6739fe94b --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_8/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..2ed3f5b06 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_8/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..bd6f890cb --- /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/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..13d467742 --- /dev/null +++ b/butane/config/openshift/v4_8/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..37e49f302 --- /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..1953040ac --- /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/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..9f2a8eb78 --- /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/butane/config/common" + "github.com/coreos/butane/config/openshift/v4_9/result" + cutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..63368afb6 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_3" + "github.com/coreos/butane/config/common" + fcos "github.com/coreos/butane/config/fcos/v1_3" + "github.com/coreos/butane/config/openshift/v4_9/result" + confutil "github.com/coreos/butane/config/util" + "github.com/coreos/butane/translate" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_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..8d3059b1b --- /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/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..4223194dc --- /dev/null +++ b/butane/config/openshift/v4_9/validate_test.go @@ -0,0 +1,252 @@ +// 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/butane/base/util" + "github.com/coreos/butane/config/common" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +func TestValidateMetadata(t *testing.T) { + tests := []struct { + in Metadata + out error + errPath path.ContextPath + }{ + // missing name + { + Metadata{ + Labels: map[string]string{ + ROLE_LABEL_KEY: "r", + }, + }, + common.ErrNameRequired, + path.New("yaml", "name"), + }, + // missing role + { + Metadata{ + Name: "n", + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + // empty role + { + Metadata{ + Name: "n", + Labels: map[string]string{ + ROLE_LABEL_KEY: "", + }, + }, + common.ErrRoleRequired, + path.New("yaml", "labels"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +func TestValidateOpenShift(t *testing.T) { + tests := []struct { + in OpenShift + out error + errPath path.ContextPath + }{ + // empty struct + { + OpenShift{}, + nil, + path.New("yaml"), + }, + // bad kernel type + { + OpenShift{ + KernelType: util.StrToPtr("hurd"), + }, + common.ErrInvalidKernelType, + path.New("yaml", "kernel_type"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + +// TestReportCorrelation tests that errors are correctly correlated to their source lines +func TestReportCorrelation(t *testing.T) { + tests := []struct { + in string + message string + line int64 + }{ + // Butane unused key check + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + q: z`, + "unused key q", + 9, + }, + // Butane YAML validation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + source: https://example.com + inline: z`, + common.ErrTooManyResourceSources.Error(), + 10, + }, + // Butane YAML validation warning + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + mode: 444`, + common.ErrDecimalMode.Error(), + 9, + }, + // Butane translation error + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: /z + contents: + local: z`, + common.ErrNoFilesDir.Error(), + 10, + }, + // Ignition validation error, leaf node + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + files: + - path: z`, + errors.ErrPathRelative.Error(), + 8, + }, + // Ignition validation error, partition + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - start_mib: 5`, + errors.ErrNeedLabelOrNumber.Error(), + 10, + }, + // Ignition validation error, partition list + { + ` + metadata: + name: something + labels: + machineconfiguration.openshift.io/role: r + storage: + disks: + - device: /dev/z + partitions: + - number: 1 + should_exist: false + - label: z`, + errors.ErrZeroesWithShouldNotExist.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..55c855a7d --- /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/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..374ec88c6 --- /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/butane/config/common" + cutil "github.com/coreos/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..c4b63d85b --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_4" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..5305452b0 --- /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/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..68e46113f --- /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/butane/config/common" + cutil "github.com/coreos/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..7b9138136 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_5" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..52726e210 --- /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/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..fc8c38214 --- /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/butane/config/common" + cutil "github.com/coreos/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..f81f21dc9 --- /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/butane/base/util" + base "github.com/coreos/butane/base/v0_8_exp" + "github.com/coreos/butane/config/common" + confutil "github.com/coreos/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..fe3ced781 --- /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..6314fd637 --- /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..997b2fb1f --- /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/butane/config/common" + "github.com/coreos/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..145b27b65 --- /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/butane/config/common" + "github.com/coreos/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 `-` where type is `sha512`. + * **_replace_** (object): the config that will replace the current. + * **source** (string): the URL of the config. Supported schemes are `http`, `https`, `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 `-` where type is `sha512`. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`. + * **source** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. 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 certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is `sha512`. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. 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. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is `sha512`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. 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. Mutually exclusive with `inline`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is `sha512`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-fcos-v1_1.md b/butane/docs/config-fcos-v1_1.md new file mode 100644 index 000000000..37945edf0 --- /dev/null +++ b/butane/docs/config-fcos-v1_1.md @@ -0,0 +1,169 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.1.0 +parent: Configuration specifications +nav_order: 48 +--- + +# Fedora CoreOS Specification v1.1.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.1.0` and generates Ignition configs with version `3.1.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. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_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. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. 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. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. 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. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. 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. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-fcos-v1_2.md b/butane/docs/config-fcos-v1_2.md new file mode 100644 index 000000000..db24525fc --- /dev/null +++ b/butane/docs/config-fcos-v1_2.md @@ -0,0 +1,199 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.2.0 +parent: Configuration specifications +nav_order: 47 +--- + +# Fedora CoreOS Specification v1.2.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.2.0` and generates Ignition configs with version `3.2.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`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-fcos-v1_3.md b/butane/docs/config-fcos-v1_3.md new file mode 100644 index 000000000..6bc86bd61 --- /dev/null +++ b/butane/docs/config-fcos-v1_3.md @@ -0,0 +1,209 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.3.0 +parent: Configuration specifications +nav_order: 46 +--- + +# Fedora CoreOS Specification v1.3.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.3.0` and generates Ignition configs with version `3.2.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`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. diff --git a/butane/docs/config-fcos-v1_4.md b/butane/docs/config-fcos-v1_4.md new file mode 100644 index 000000000..fafd64961 --- /dev/null +++ b/butane/docs/config-fcos-v1_4.md @@ -0,0 +1,212 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.4.0 +parent: Configuration specifications +nav_order: 45 +--- + +# Fedora CoreOS Specification v1.4.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.4.0` and generates Ignition configs with version `3.3.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`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. diff --git a/butane/docs/config-fcos-v1_5.md b/butane/docs/config-fcos-v1_5.md new file mode 100644 index 000000000..f523f0d88 --- /dev/null +++ b/butane/docs/config-fcos-v1_5.md @@ -0,0 +1,224 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.5.0 +parent: Configuration specifications +nav_order: 44 +--- + +# Fedora CoreOS Specification v1.5.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.5.0` and generates Ignition configs with version `3.4.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): describes the desired GRUB bootloader configuration. + * **_users_** (list of objects): the list of GRUB superusers. + * **name** (string): the user name. + * **password_hash** (string): the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. diff --git a/butane/docs/config-fcos-v1_6.md b/butane/docs/config-fcos-v1_6.md new file mode 100644 index 000000000..5bbba7cff --- /dev/null +++ b/butane/docs/config-fcos-v1_6.md @@ -0,0 +1,229 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.6.0 +parent: Configuration specifications +nav_order: 43 +--- + +# Fedora CoreOS Specification v1.6.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.6.0` and generates Ignition configs with version `3.5.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): describes the desired GRUB bootloader configuration. + * **_users_** (list of objects): the list of GRUB superusers. + * **name** (string): the user name. + * **password_hash** (string): the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. diff --git a/butane/docs/config-fcos-v1_7.md b/butane/docs/config-fcos-v1_7.md new file mode 100644 index 000000000..433092298 --- /dev/null +++ b/butane/docs/config-fcos-v1_7.md @@ -0,0 +1,237 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.7.0 +parent: Configuration specifications +nav_order: 42 +--- + +# Fedora CoreOS Specification v1.7.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.7.0` and generates Ignition configs with version `3.6.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): describes the desired GRUB bootloader configuration. + * **_users_** (list of objects): the list of GRUB superusers. + * **name** (string): the user name. + * **password_hash** (string): the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. diff --git a/butane/docs/config-fcos-v1_8-exp.md b/butane/docs/config-fcos-v1_8-exp.md new file mode 100644 index 000000000..f00a1ff70 --- /dev/null +++ b/butane/docs/config-fcos-v1_8-exp.md @@ -0,0 +1,248 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora CoreOS v1.8.0-experimental +parent: Configuration specifications +nav_order: 50 +--- + +# Fedora CoreOS Specification v1.8.0-experimental + +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +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.8.0-experimental` and generates Ignition configs with version `3.7.0-experimental`. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): describes the desired GRUB bootloader configuration. + * **_users_** (list of objects): the list of GRUB superusers. + * **name** (string): the user name. + * **password_hash** (string): the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. diff --git a/butane/docs/config-fiot-v1_0.md b/butane/docs/config-fiot-v1_0.md new file mode 100644 index 000000000..b59fdf2b5 --- /dev/null +++ b/butane/docs/config-fiot-v1_0.md @@ -0,0 +1,146 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora IoT v1.0.0 +parent: Configuration specifications +nav_order: 249 +--- + +# Fedora IoT Specification v1.0.0 + +The Fedora IoT 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 `%VARIANT%` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `%VERSION%` and generates Ignition configs with version `3.4.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-fiot-v1_1-exp.md b/butane/docs/config-fiot-v1_1-exp.md new file mode 100644 index 000000000..d91d04341 --- /dev/null +++ b/butane/docs/config-fiot-v1_1-exp.md @@ -0,0 +1,165 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Fedora IoT v1.1.0-experimental +parent: Configuration specifications +nav_order: 250 +--- + +# Fedora IoT Specification v1.1.0-experimental + +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +The Fedora IoT 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 `%VARIANT%` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `%VERSION%` and generates Ignition configs with version `3.7.0-experimental`. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-flatcar-v1_0.md b/butane/docs/config-flatcar-v1_0.md new file mode 100644 index 000000000..ac9533ddb --- /dev/null +++ b/butane/docs/config-flatcar-v1_0.md @@ -0,0 +1,192 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Flatcar v1.0.0 +parent: Configuration specifications +nav_order: 99 +--- + +# Flatcar Specification v1.0.0 + +The Flatcar 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 `flatcar` 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.3.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`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. diff --git a/butane/docs/config-flatcar-v1_1.md b/butane/docs/config-flatcar-v1_1.md new file mode 100644 index 000000000..7ede42da2 --- /dev/null +++ b/butane/docs/config-flatcar-v1_1.md @@ -0,0 +1,197 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Flatcar v1.1.0 +parent: Configuration specifications +nav_order: 98 +--- + +# Flatcar Specification v1.1.0 + +The Flatcar 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 `flatcar` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.1.0` and generates Ignition configs with version `3.4.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. diff --git a/butane/docs/config-flatcar-v1_2-exp.md b/butane/docs/config-flatcar-v1_2-exp.md new file mode 100644 index 000000000..c2dfab33b --- /dev/null +++ b/butane/docs/config-flatcar-v1_2-exp.md @@ -0,0 +1,227 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: Flatcar v1.2.0-experimental +parent: Configuration specifications +nav_order: 100 +--- + +# Flatcar Specification v1.2.0-experimental + +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +The Flatcar 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 `flatcar` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.2.0-experimental` and generates Ignition configs with version `3.7.0-experimental`. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, btrfs, xfs, vfat, swap, or none). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. +* **_kernel_arguments_** (object): describes the desired kernel arguments. + * **_should_exist_** (list of strings): the list of kernel arguments that should exist. + * **_should_not_exist_** (list of strings): the list of kernel arguments that should not exist. diff --git a/butane/docs/config-openshift-v4_10.md b/butane/docs/config-openshift-v4_10.md new file mode 100644 index 000000000..1324c5834 --- /dev/null +++ b/butane/docs/config-openshift-v4_10.md @@ -0,0 +1,165 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.10.0 +parent: Configuration specifications +nav_order: 147 +--- + +# OpenShift Specification v4.10.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.10.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13) in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_11.md b/butane/docs/config-openshift-v4_11.md new file mode 100644 index 000000000..830d6450d --- /dev/null +++ b/butane/docs/config-openshift-v4_11.md @@ -0,0 +1,165 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.11.0 +parent: Configuration specifications +nav_order: 146 +--- + +# OpenShift Specification v4.11.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.11.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13) in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_12.md b/butane/docs/config-openshift-v4_12.md new file mode 100644 index 000000000..1cd2b55f1 --- /dev/null +++ b/butane/docs/config-openshift-v4_12.md @@ -0,0 +1,165 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.12.0 +parent: Configuration specifications +nav_order: 145 +--- + +# OpenShift Specification v4.12.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.12.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13) in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_13.md b/butane/docs/config-openshift-v4_13.md new file mode 100644 index 000000000..b4dfb000e --- /dev/null +++ b/butane/docs/config-openshift-v4_13.md @@ -0,0 +1,166 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.13.0 +parent: Configuration specifications +nav_order: 144 +--- + +# OpenShift Specification v4.13.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.13.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_14.md b/butane/docs/config-openshift-v4_14.md new file mode 100644 index 000000000..4b97dbd20 --- /dev/null +++ b/butane/docs/config-openshift-v4_14.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.14.0 +parent: Configuration specifications +nav_order: 143 +--- + +# OpenShift Specification v4.14.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.14.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_15.md b/butane/docs/config-openshift-v4_15.md new file mode 100644 index 000000000..5dd4e431d --- /dev/null +++ b/butane/docs/config-openshift-v4_15.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.15.0 +parent: Configuration specifications +nav_order: 142 +--- + +# OpenShift Specification v4.15.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.15.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_16.md b/butane/docs/config-openshift-v4_16.md new file mode 100644 index 000000000..42477e7b0 --- /dev/null +++ b/butane/docs/config-openshift-v4_16.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.16.0 +parent: Configuration specifications +nav_order: 141 +--- + +# OpenShift Specification v4.16.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.16.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_17.md b/butane/docs/config-openshift-v4_17.md new file mode 100644 index 000000000..ed2afafbd --- /dev/null +++ b/butane/docs/config-openshift-v4_17.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.17.0 +parent: Configuration specifications +nav_order: 140 +--- + +# OpenShift Specification v4.17.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.17.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_18.md b/butane/docs/config-openshift-v4_18.md new file mode 100644 index 000000000..f793c344a --- /dev/null +++ b/butane/docs/config-openshift-v4_18.md @@ -0,0 +1,178 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.18.0 +parent: Configuration specifications +nav_order: 139 +--- + +# OpenShift Specification v4.18.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.18.0` and generates Ignition configs with version `3.4.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_19.md b/butane/docs/config-openshift-v4_19.md new file mode 100644 index 000000000..2a6dee8ac --- /dev/null +++ b/butane/docs/config-openshift-v4_19.md @@ -0,0 +1,183 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.19.0 +parent: Configuration specifications +nav_order: 138 +--- + +# OpenShift Specification v4.19.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.19.0` and generates Ignition configs with version `3.5.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_20.md b/butane/docs/config-openshift-v4_20.md new file mode 100644 index 000000000..2f1daa08c --- /dev/null +++ b/butane/docs/config-openshift-v4_20.md @@ -0,0 +1,183 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.20.0 +parent: Configuration specifications +nav_order: 137 +--- + +# OpenShift Specification v4.20.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.20.0` and generates Ignition configs with version `3.5.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_21.md b/butane/docs/config-openshift-v4_21.md new file mode 100644 index 000000000..19b47ae90 --- /dev/null +++ b/butane/docs/config-openshift-v4_21.md @@ -0,0 +1,183 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.21.0 +parent: Configuration specifications +nav_order: 136 +--- + +# OpenShift Specification v4.21.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.21.0` and generates Ignition configs with version `3.5.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_22.md b/butane/docs/config-openshift-v4_22.md new file mode 100644 index 000000000..e0a1b23bb --- /dev/null +++ b/butane/docs/config-openshift-v4_22.md @@ -0,0 +1,191 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.22.0 +parent: Configuration specifications +nav_order: 135 +--- + +# OpenShift Specification v4.22.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.22.0` and generates Ignition configs with version `3.6.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): Unsupported + * **_users_** (list of objects): Unsupported + * **name** (string): Unsupported + * **password_hash** (string): Unsupported +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_23-exp.md b/butane/docs/config-openshift-v4_23-exp.md new file mode 100644 index 000000000..a6ea08f0e --- /dev/null +++ b/butane/docs/config-openshift-v4_23-exp.md @@ -0,0 +1,202 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.23.0-experimental +parent: Configuration specifications +nav_order: 150 +--- + +# OpenShift Specification v4.23.0-experimental + +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.23.0-experimental` and generates Ignition configs with version `3.7.0-experimental`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_open_options_** (list of strings): any additional options to be passed to `cryptsetup luksOpen`. Supported options will be persistently written to the luks volume. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_device_** (string): the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_advertisement_** (string): the advertisement JSON. If not specified, the advertisement is fetched from the tang server during provisioning. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_discard_** (boolean): whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + * **_cex_** (object): describes the IBM Crypto Express (CEX) card configuration for the luks device. + * **_enabled_** (boolean): whether or not to enable cex compatibility for luks. If omitted, defaults to false. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_grub_** (object): describes the desired GRUB bootloader configuration. + * **_users_** (list of objects): the list of GRUB superusers. + * **name** (string): the user name. + * **password_hash** (string): the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_8.md b/butane/docs/config-openshift-v4_8.md new file mode 100644 index 000000000..40599839b --- /dev/null +++ b/butane/docs/config-openshift-v4_8.md @@ -0,0 +1,164 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.8.0 +parent: Configuration specifications +nav_order: 149 +--- + +# OpenShift Specification v4.8.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.8.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13) in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-openshift-v4_9.md b/butane/docs/config-openshift-v4_9.md new file mode 100644 index 000000000..6655b5006 --- /dev/null +++ b/butane/docs/config-openshift-v4_9.md @@ -0,0 +1,164 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: OpenShift v4.9.0 +parent: Configuration specifications +nav_order: 148 +--- + +# OpenShift Specification v4.9.0 + +The OpenShift configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ +* **variant** (string): used to differentiate configs for different operating systems. Must be `openshift` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `4.9.0` and generates Ignition configs with version `3.2.0`. +* **metadata** (object): metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **name** (string): a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + * **labels** (object): string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_disks_** (list of objects): the list of disks to be configured and their options. Every entry must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_wipe_table_** (boolean): whether or not the partition tables shall be wiped. When true, the partition tables are erased before any further manipulation. Otherwise, the existing entries are left intact. + * **_partitions_** (list of objects): the list of partitions and their configuration for this particular disk. Every partition must have a unique `number`, or if 0 is specified, a unique `label`. + * **_label_** (string): the PARTLABEL for the partition. + * **_number_** (integer): the partition number, which dictates its position in the partition table (one-indexed). If zero, use the next available partition slot. + * **_size_mib_** (integer): the size of the partition (in mebibytes). If zero, the partition will be made as large as possible. + * **_start_mib_** (integer): the start of the partition (in mebibytes). If zero, the partition will be positioned at the start of the largest block available. + * **_type_guid_** (string): the GPT [partition type GUID](https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs). If omitted, the default will be 0FC63DAF-8483-4772-8E79-3D69D8477DE4 (Linux filesystem data). + * **_guid_** (string): the GPT unique partition GUID. + * **_wipe_partition_entry_** (boolean): if true, Ignition will clobber an existing partition if it does not match the config. If false (default), Ignition will fail instead. + * **_should_exist_** (boolean): whether or not the partition with the specified `number` should exist. If omitted, it defaults to true. If false Ignition will either delete the specified partition or fail, depending on `wipePartitionEntry`. If false `number` must be specified and non-zero and `label`, `start`, `size`, `guid`, and `typeGuid` must all be omitted. + * **_resize_** (boolean): whether or not the existing partition should be resized. If omitted, it defaults to false. If true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + * **_raid_** (list of objects): the list of RAID arrays to be configured. Every RAID array must have a unique `name`. + * **name** (string): the name to use for the resulting md device. + * **level** (string): the redundancy level of the array (e.g. linear, raid1, raid5, etc.). + * **devices** (list of strings): the list of devices (referenced by their absolute path) in the array. + * **_spares_** (integer): the number of spares (if applicable) in the array. + * **_options_** (list of strings): any additional options to be passed to mdadm. + * **_filesystems_** (list of objects): the list of filesystems to be configured. `device` and `format` need to be specified. Every filesystem must have a unique `device`. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **format** (string): the filesystem format (ext4, xfs, vfat, or swap). + * **_path_** (string): the mount-point of the filesystem while Ignition is running relative to where the root filesystem will be mounted. This is not necessarily the same as where it should be mounted in the real root, but it is encouraged to make it the same. + * **_wipe_filesystem_** (boolean): whether or not to wipe the device before filesystem creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. Defaults to false. + * **_label_** (string): the label of the filesystem. + * **_uuid_** (string): the uuid of the filesystem. + * **_options_** (list of strings): any additional options to be passed to the format-specific mkfs utility. + * **_mount_options_** (list of strings): any special options to be passed to the mount command. + * **_with_mount_unit_** (boolean): whether to additionally generate a generic mount unit for this filesystem. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path`. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_luks_** (list of objects): the list of luks devices to be created. Every device must have a unique `name`. + * **name** (string): the name of the luks device. + * **device** (string): the absolute path to the device. Devices are typically referenced by the `/dev/disk/by-*` symlinks. + * **_key_file_** (object): options related to the contents of the key file. + * **_source_** (string): the URL of the key file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the key file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the key file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the key file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the key file. + * **_hash_** (string): the hash of the key file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed key file. + * **_label_** (string): the label of the luks device. + * **_uuid_** (string): the uuid of the luks device. + * **_options_** (list of strings): any additional options to be passed to `cryptsetup luksFormat`. + * **_wipe_volume_** (boolean): whether or not to wipe the device before volume creation, see [Ignition's documentation on filesystems](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + * **_clevis_** (object): describes the clevis configuration for the luks device. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_custom_** (object): overrides the clevis configuration. The `pin` & `config` will be passed directly to `clevis luks bind`. If specified, all other clevis options must be omitted. + * **pin** (string): the clevis pin. + * **config** (string): the clevis configuration JSON. + * **_needs_network_** (boolean): whether or not the device requires networking. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Symlinks must not be present. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. File attributes can be overridden by creating a corresponding entry in the `files` section; such entries must omit `contents`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. Must be `core`. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13) in the user's home directory. All SSH keys must be unique. +* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + * **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + * **_luks_** (object): describes the clevis configuration for encrypting the root filesystem. + * **_tang_** (list of objects): describes a tang server. Every server must have a unique `url`. + * **url** (string): url of the tang server. + * **thumbprint** (string): thumbprint of a trusted signing key. + * **_tpm2_** (boolean): whether or not to use a tpm2 device. + * **_threshold_** (integer): sets the minimum number of pieces required to decrypt the device. Default is 1. + * **_mirror_** (object): describes mirroring of the boot disk for fault tolerance. + * **_devices_** (list of strings): the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. +* **_openshift_** (object): describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + * **_kernel_type_** (string): which kernel to use on the node. Must be `default` or `realtime`. + * **_kernel_arguments_** (list of strings): arguments to be added to the kernel command line. + * **_extensions_** (list of strings): RHCOS extensions to be installed on the node. + * **_fips_** (boolean): whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/docs/config-r4e-v1_0.md b/butane/docs/config-r4e-v1_0.md new file mode 100644 index 000000000..5ad6a0841 --- /dev/null +++ b/butane/docs/config-r4e-v1_0.md @@ -0,0 +1,143 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: RHEL for Edge v1.0.0 +parent: Configuration specifications +nav_order: 199 +--- + +# RHEL for Edge Specification v1.0.0 + +The RHEL for Edge 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 `r4e` 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.3.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`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-r4e-v1_1.md b/butane/docs/config-r4e-v1_1.md new file mode 100644 index 000000000..398590035 --- /dev/null +++ b/butane/docs/config-r4e-v1_1.md @@ -0,0 +1,146 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: RHEL for Edge v1.1.0 +parent: Configuration specifications +nav_order: 198 +--- + +# RHEL for Edge Specification v1.1.0 + +The RHEL for Edge 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 `r4e` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.1.0` and generates Ignition configs with version `3.4.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`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are not supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership is not preserved. File modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/config-r4e-v1_2-exp.md b/butane/docs/config-r4e-v1_2-exp.md new file mode 100644 index 000000000..9aeceab40 --- /dev/null +++ b/butane/docs/config-r4e-v1_2-exp.md @@ -0,0 +1,165 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: RHEL for Edge v1.2.0-experimental +parent: Configuration specifications +nav_order: 200 +--- + +# RHEL for Edge Specification v1.2.0-experimental + +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +The RHEL for Edge 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 `r4e` for this specification. +* **version** (string): the semantic version of the spec for this document. This document is for version `1.2.0-experimental` and generates Ignition configs with version `3.7.0-experimental`. +* **_ignition_** (object): metadata about the configuration itself. + * **_config_** (object): options related to the configuration. + * **_merge_** (list of objects): a list of the configs to be merged to the current config. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_replace_** (object): the config that will replace the current. + * **_source_** (string): the URL of the config. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the config. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the config, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the config (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the config. + * **_hash_** (string): the hash of the config, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed config. + * **_timeouts_** (object): options relating to `http` timeouts when fetching files over `http` or `https`. + * **_http_response_headers_** (integer): the time to wait (in seconds) for the server's response headers (but not the body) after making a request. 0 indicates no timeout. Default is 10 seconds. + * **_http_total_** (integer): the time limit (in seconds) for the operation (connection, request, and response), including retries. 0 indicates no timeout. Default is 0. + * **_security_** (object): options relating to network security. + * **_tls_** (object): options relating to TLS when fetching resources over `https`. + * **_certificate_authorities_** (list of objects): the list of additional certificate authorities (in addition to the system authorities) to be used for TLS verification when fetching over `https`. All certificate authorities must have a unique `source`, `inline`, or `local`. + * **_source_** (string): the URL of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the certificate bundle (in PEM format), relative to the directory specified by the `--files-dir` command-line argument. The bundle can contain multiple concatenated certificates. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the certificate bundle (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the certificate bundle. + * **_hash_** (string): the hash of the certificate bundle, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed certificate bundle. + * **_proxy_** (object): options relating to setting an `HTTP(S)` proxy when fetching resources. + * **_http_proxy_** (string): will be used as the proxy URL for HTTP requests and HTTPS requests unless overridden by `https_proxy` or `no_proxy`. + * **_https_proxy_** (string): will be used as the proxy URL for HTTPS requests unless overridden by `no_proxy`. + * **_no_proxy_** (list of strings): specifies a list of strings to hosts that should be excluded from proxying. Each value is represented by an `IP address prefix (1.2.3.4)`, `an IP address prefix in CIDR notation (1.2.3.4/8)`, `a domain name`, or `a special DNS label (*)`. An IP address prefix and domain name can also include a literal port number `(1.2.3.4:80)`. A domain name matches that name and all subdomains. A domain name with a leading `.` matches subdomains only. For example `foo.com` matches `foo.com` and `bar.foo.com`; `.y.com` matches `x.y.com` but not `y.com`. A single asterisk `(*)` indicates that no proxying should be done. +* **_storage_** (object): describes the desired state of the system's storage devices. + * **_files_** (list of objects): the list of files to be written. Every file, directory and link must have a unique `path`. + * **path** (string): the absolute path to the file. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. `contents` must be specified if `overwrite` is true. Defaults to false. + * **_contents_** (object): options related to the contents of the file. + * **_source_** (string): the URL of the file. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. If source is omitted and a regular file already exists at the path, Ignition will do nothing. If source is omitted and no file exists, an empty file will be created. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the file. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the file (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the file. + * **_hash_** (string): the hash of the file, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed file. + * **_append_** (list of objects): list of fragments to be appended to the file. Follows the same structure as `contents`. + * **_source_** (string): the URL of the fragment. Supported schemes are `http`, `https`, `tftp`, `s3`, `arn`, `gs`, and [`data`](https://tools.ietf.org/html/rfc2397). When using `http`, it is advisable to use the verification option to ensure the contents haven't been modified. Mutually exclusive with `inline` and `local`. + * **_inline_** (string): the contents of the fragment. Mutually exclusive with `source` and `local`. + * **_local_** (string): a local path to the contents of the fragment, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`. + * **_compression_** (string): the type of compression used on the fragment (null or gzip). Compression cannot be used with S3. + * **_http_headers_** (list of objects): a list of HTTP headers to be added to the request. Available for `http` and `https` source schemes only. + * **name** (string): the header name. + * **_value_** (string): the header contents. + * **_verification_** (object): options related to the verification of the fragment. + * **_hash_** (string): the hash of the fragment, in the form `-` where type is either `sha512` or `sha256`. If `compression` is specified, the hash describes the decompressed fragment. + * **_mode_** (integer): the file's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for files defaults to 0644 or the existing file's permissions if `overwrite` is false, `contents` is unspecified, and a file already exists at the path. + * **_user_** (object): specifies the file's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the file's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_directories_** (list of objects): the list of directories to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the directory. + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If false and a directory already exists at the path, Ignition will only set its permissions. If false and a non-directory exists at that path, Ignition will fail. Defaults to false. + * **_mode_** (integer): the directory's permission mode. Setuid/setgid/sticky bits are supported. If not specified, the permission mode for directories defaults to 0755 or the mode of an existing directory if `overwrite` is false and a directory already exists at the path. + * **_user_** (object): specifies the directory's owner. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the directory's group. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **_links_** (list of objects): the list of links to be created. Every file, directory, and link must have a unique `path`. + * **path** (string): the absolute path to the link + * **_overwrite_** (boolean): whether to delete preexisting nodes at the path. If overwrite is false and a matching link exists at the path, Ignition will only set the owner and group. Defaults to false. + * **_user_** (object): specifies the owner for a symbolic link. Ignored for hard links. + * **_id_** (integer): the user ID of the owner. + * **_name_** (string): the user name of the owner. + * **_group_** (object): specifies the group for a symbolic link. Ignored for hard links. + * **_id_** (integer): the group ID of the group. + * **_name_** (string): the group name of the group. + * **target** (string): the target path of the link + * **_hard_** (boolean): a symbolic link is created if this is false, a hard one if this is true. + * **_trees_** (list of objects): a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + * **local** (string): the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + * **_path_** (string): the path of the tree within the target system. Defaults to `/`. + * **_file_mode_** (integer): Custom permissions to apply to files + * **_dir_mode_** (integer): Custom permissions to apply to directories + * **_user_** (object): User owner of the tree + * **_name_** (string): username + * **_id_** (integer): uid + * **_group_** (object): Group owner of the tree + * **_name_** (string): group name + * **_id_** (integer): gid +* **_systemd_** (object): describes the desired state of the systemd units. + * **_units_** (list of objects): the list of systemd units. Every unit must have a unique `name`. + * **name** (string): the name of the unit. This must be suffixed with a valid unit type (e.g. "thing.service"). + * **_enabled_** (boolean): whether or not the service shall be enabled. When true, the service is enabled. When false, the service is disabled. When omitted, the service is unmodified. In order for this to have any effect, the unit must have an install section. + * **_mask_** (boolean): whether or not the service shall be masked. When true, the service is masked by symlinking it to `/dev/null`. When false, the service is unmasked by deleting the symlink to `/dev/null` if it exists. + * **_contents_** (string): the contents of the unit. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`. + * **name** (string): the name of the drop-in. This must be suffixed with ".conf". + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. +* **_passwd_** (object): describes the desired additions to the passwd database. + * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. + * **name** (string): the username for the account. + * **_password_hash_** (string): the hashed password for the account. + * **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. + * **_ssh_authorized_keys_local_** (list of strings): a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line. + * **_uid_** (integer): the user ID of the account. + * **_gecos_** (string): the GECOS field of the account. + * **_home_dir_** (string): the home directory of the account. + * **_no_create_home_** (boolean): whether or not to create the user's home directory. This only has an effect if the account doesn't exist yet. + * **_primary_group_** (string): the name of the primary group of the account. + * **_groups_** (list of strings): the list of supplementary groups of the account. + * **_no_user_group_** (boolean): whether or not to create a group with the same name as the user. This only has an effect if the account doesn't exist yet. + * **_no_log_init_** (boolean): whether or not to add the user to the lastlog and faillog databases. This only has an effect if the account doesn't exist yet. + * **_shell_** (string): the login shell of the new account. + * **_should_exist_** (boolean): whether or not the user with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified user. + * **_system_** (boolean): whether or not this account should be a system account. This only has an effect if the account doesn't exist yet. + * **_groups_** (list of objects): the list of groups to be added. All groups must have a unique `name`. + * **name** (string): the name of the group. + * **_gid_** (integer): the group ID of the new group. + * **_password_hash_** (string): the hashed password of the new group. + * **_should_exist_** (boolean): whether or not the group with the specified `name` should exist. If omitted, it defaults to true. If false, then Ignition will delete the specified group. + * **_system_** (boolean): whether or not the group should be a system group. This only has an effect if the group doesn't exist yet. diff --git a/butane/docs/development.md b/butane/docs/development.md new file mode 100644 index 000000000..b16740712 --- /dev/null +++ b/butane/docs/development.md @@ -0,0 +1,85 @@ +--- +nav_order: 10 +--- + +# Developing Butane +{: .no_toc } + +1. TOC +{:toc} + +## Project layout + +Internally, Butane has a versioned `base` component which contains support for +a particular Ignition spec version, plus distro-independent sugar. New base +functionality is added only to the experimental base package. Eventually the +experimental base package is stabilized and a new experimental package +created. The base component is versioned independently of any particular +distro, and its versions are not exposed to the user. Client code should +not need to import anything from `base`. + +Each config variant/version pair corresponds to a `config` package, which +derives either from a `base` package or from another `config` package. New +functionality is similarly added only to an experimental config version, +which is eventually stabilized and a new experimental version created. +(This will often happen when the underlying package is stabilized.) A +`config` package can contain sugar or validation logic specific to a distro +(for example, additional properties for configuring etcd). + +Packages outside the Butane repository can implement additional config versions +by deriving from a `base` or `config` package and registering their +variant/version pair with `config`. + +- `config/` — + Top-level `TranslateBytes()` function that determines which config version + to parse and emit. Clients should typically use this to translate configs. + +- `config/common/` — + Common definitions for all spec versions, including translate options + structs and error definitions. + +- `config/*/vX_Y/` — + User facing definitions of the spec. Each is derived from another config + package or from a base package. Each one defines its own translate + functions to be registered in the `config` package. Clients can use + these directly if they want to translate a specific spec version. + +- `config/util/` — + Utility code for implementing config packages, including the + (un)marshaling helpers. Clients don't need to import this unless they're + implementing an out-of-tree config version. + +- `base/` — + Distro-agnostic code targeting individual Ignition spec versions. Clients + don't need to import this unless they're implementing an out-of-tree + config version. + +- `internal/` — + `main`, non-exported code. + +## Adding sugar + +Sugar implementations should generally translate the sugar into a fresh Ignition config struct, then use Ignition config merging to merge that struct with the user's config. The desugared struct should be the merge parent and the user's config the child, allowing the user to override field values produced by desugaring. + +This approach may not always be suitable, since Ignition's config merging isn't always expressive enough. In that case, it may be necessary to directly modify the user's Ignition config struct. + +## Creating a release + +Create a [release checklist](https://github.com/coreos/butane/issues/new?template=release-checklist.md) and follow those steps. + +## The build process + +Note that the binaries released in this repository are not built using the `build` script from this repository +but using a `butane.spec` maintained in [Fedora rpms/butane](https://src.fedoraproject.org/rpms/butane). +This build process uses the [go-rpm-macros](https://pagure.io/go-rpm-macros) to set up the Go build environment and is +subject to the [Golang Packaging Guidelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/Golang/). + +Consult the [Package Maintenance Guide](https://docs.fedoraproject.org/en-US/package-maintainers/Package_Maintenance_Guide/) +and the [Pull Requests Guide](https://docs.fedoraproject.org/en-US/ci/pull-requests/) if you want to contribute to the build process. + +In case you have trouble with the aforementioned standard Pull Request Guide, consult the Pagure documentation on the +[Remote Git to Pagure pull request](https://docs.pagure.org/pagure/usage/pull_requests.html#remote-git-to-pagure-pull-request) workflow. + +## Bumping spec versions + +Create a new [stabilization checklist](https://github.com/coreos/butane/issues/new?template=stabilize-checklist.md) and follow the steps there. diff --git a/butane/docs/examples.md b/butane/docs/examples.md new file mode 100644 index 000000000..68e9a2f66 --- /dev/null +++ b/butane/docs/examples.md @@ -0,0 +1,548 @@ +--- +nav_order: 3 +--- + +# Examples +{: .no_toc } + +1. TOC +{:toc} + +Here you can find a bunch of simple examples for using Butane, with some explanations about what they do. The examples here are in no way comprehensive, for a full list of all the options present in Butane check out the [configuration specification][spec]. + +## Users and groups + +This example modifies the existing `core` user and sets its SSH key by providing it inline. + + +```yaml +variant: fcos +version: 1.1.0 +passwd: + users: + - name: core + ssh_authorized_keys: + - key1 +``` + +This example creates one user, `user1` and sets up one SSH public key for the user. The user is also given the home directory `/home/user1`, but it's not created, the user is added to the `wheel` and `plugdev` groups, and the user's shell is set to `/bin/bash`. + + +```yaml +variant: fcos +version: 1.1.0 +passwd: + users: + - name: user1 + ssh_authorized_keys: + - key1 + home_dir: /home/user1 + no_create_home: true + groups: + - wheel + - plugdev + shell: /bin/bash +``` + +### Using password authentication + +You can use a Butane config to set a password for a local user. Building on the previous example, we can configure the `password_hash` for one or more users: + + +```yaml +variant: fcos +version: 1.1.0 +passwd: + users: + - name: user1 + ssh_authorized_keys: + - key1 + password_hash: $y$j9T$aUmgEDoFIDPhGxEe2FUjc/$C5A... + home_dir: /home/user1 + no_create_home: true + groups: + - wheel + - plugdev + shell: /bin/bash +``` + +To generate a secure password hash, use `mkpasswd` from the `whois` package. Your Linux distro may ship a different `mkpasswd` implementation; you can ensure you're using the correct one by running it from a container: + +``` +$ podman run -ti --rm quay.io/coreos/mkpasswd --method=yescrypt +Password: +$y$j9T$A0Y3wwVOKP69S.1K/zYGN.$S596l11UGH3XjN... +``` + +The `yescrypt` hashing method is recommended for new passwords. For more details on hashing methods, see `man 5 crypt`. + +For more information, see the Fedora CoreOS documentation on [Authentication][fcos-auth-docs]. + +### SSH keys from local files + +As shown in the previous examples you can inline multiple SSH public keys per user directly in the Butane config. Additionally, you can embed keys from local files at transpile time. This example creates a `core` user and configures its SSH keys from local files. The file paths are relative to the directory specified with the `--files-dir` command-line option, which must be provided. + + +```yaml +variant: fcos +version: 1.5.0 +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub + - id_ed25519.pub +``` + +Different combinations for providing SSH keys are possible. You can provide inline ones together with file references and the files may also contain multiple keys (one per line). However, keep in mind that overall the keys must be unique. Check the [configuration specification][spec] for details. + +## Storage and files + +### Files + +This example creates a file at `/opt/file` with the contents `Hello, world!`, permissions 0644 (so readable and writable by the owner, and only readable by everyone else), and the file is owned by user uid 500 and gid 501. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + files: + - path: /opt/file + contents: + inline: Hello, world! + mode: 0644 + user: + id: 500 + group: + id: 501 +``` + +This example fetches a gzip-compressed file from `http://example.com/file2`, makes sure that the _uncompressed_ contents match the provided sha512 hash, and writes it to `/opt/file2`. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + files: + - path: /opt/file2 + contents: + source: http://example.com/file2 + compression: gzip + verification: + hash: sha512-4ee6a9d20cc0e6c7ee187daffa6822bdef7f4cebe109eff44b235f97e45dc3d7a5bb932efc841192e46618f48a6f4f5bc0d15fd74b1038abf46bf4b4fd409f2e + mode: 0644 +``` + +This example creates a file at `/opt/file3` whose contents are read from a local file `local-file3` on the system running Butane. The path of the local file is relative to a _files-dir_ which must be specified via the `-d`/`--files-dir` option to Butane. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + files: + - path: /opt/file3 + contents: + local: local-file3 + mode: 0644 +``` + +### Directory trees + +Consider a directory tree at `~/conf/tree` on the system running Butane: + +``` +file +overridden-file +directory/file +directory/symlink -> ../file +``` + +This example copies that directory tree to `/etc/files` on the target system. The ownership and mode for `overridden-file` are explicitly set by the config. All other filesystem objects are owned by `root:root`, directory modes are set to 0755, and file modes are set to 0755 if the source file is executable or 0644 otherwise. The example must be transpiled with `--files-dir ~/conf`. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + trees: + - local: tree + path: /etc/files + files: + - path: /etc/files/overridden-file + mode: 0600 + user: + id: 500 + group: + id: 501 +``` + +### Filesystems and partitions + +This example creates a single partition spanning all of the `sdb` device then creates a btrfs filesystem on it to use as `/var`. Finally, it creates the mount unit for systemd so it gets mounted on boot. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + disks: + - device: /dev/sdb + wipe_table: true + partitions: + - number: 1 + label: var + filesystems: + - path: /var + device: /dev/disk/by-partlabel/var + format: btrfs + wipe_filesystem: true + label: var + with_mount_unit: true +``` + +### Swap areas + +This example creates a swap partition spanning all of the `sdb` device, creates a swap area on it, and creates a systemd swap unit so the swap area is enabled on boot. + + +```yaml +variant: fcos +version: 1.4.0 +storage: + disks: + - device: /dev/sdb + wipe_table: true + partitions: + - number: 1 + label: swap + filesystems: + - device: /dev/disk/by-partlabel/swap + format: swap + wipe_filesystem: true + with_mount_unit: true +``` + +### LUKS encrypted storage + +This example creates three LUKS2 encrypted storage volumes: one unlocked with a static key file, one with a TPM2 device via Clevis, and one with a network Tang server via Clevis. Volumes can be unlocked with any combination of these methods, or with a custom Clevis PIN and CFG. If a key file is not specified for a device, an ephemeral one will be created. + + +```yaml +variant: fcos +version: 1.2.0 +storage: + luks: + - name: static-key-example + device: /dev/sdb + key_file: + inline: REPLACE-THIS-WITH-YOUR-KEY-MATERIAL + - name: tpm-example + device: /dev/sdc + clevis: + tpm2: true + - name: tang-example + device: /dev/sdd + clevis: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + filesystems: + - path: /var/lib/static_key_example + device: /dev/disk/by-id/dm-name-static-key-example + format: ext4 + label: STATIC-EXAMPLE + with_mount_unit: true + - path: /var/lib/tpm_example + device: /dev/disk/by-id/dm-name-tpm-example + format: ext4 + label: TPM-EXAMPLE + with_mount_unit: true + - path: /var/lib/tang_example + device: /dev/disk/by-id/dm-name-tang-example + format: ext4 + label: TANG-EXAMPLE + with_mount_unit: true +``` + +This example uses the shortcut `boot_device` syntax to configure an encrypted root filesystem unlocked with a combination of a TPM2 device and a network Tang server. + + +```yaml +variant: fcos +version: 1.3.0 +boot_device: + luks: + tpm2: true + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +This example combines `boot_device` with a manually-specified filesystem `format` to create an encrypted root filesystem formatted with `ext4` instead of the default `xfs`. + + +```yaml +variant: fcos +version: 1.3.0 +boot_device: + luks: + tpm2: true +storage: + filesystems: + - device: /dev/mapper/root + format: ext4 +``` + +This example uses the shortcut `boot_device` syntax to configure an encrypted root filesystem in s390x on the `dasda` DASD device unlocked with a network Tang server. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +This example uses the shortcut `boot_device` syntax to configure an encrypted root filesystem in s390x on the `sdb` zFCP device unlocked with a network Tang server. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-zfcp + luks: + device: /dev/sdb + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +This example uses the shortcut `boot_device` syntax to configure an encrypted root filesystem in s390x KVM unlocked with a network Tang server. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-virt + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +This example uses the shortcut `boot_device` syntax to configure an encrypted root filesystem in s390x on the `dasda` DASD device unlocked with a CEX card. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + cex: + enabled: true +``` + +### Mirrored boot disk + +This example replicates all default partitions on the boot disk across multiple disks, allowing the system to survive disk failure. + + +```yaml +variant: fcos +version: 1.3.0 +boot_device: + layout: x86_64 + mirror: + devices: + - /dev/sda + - /dev/sdb +``` + +This example configures a mirrored boot disk with a TPM2-encrypted root filesystem, overrides the size of the root partition replicas, and adds a mirrored `/var` partition which consumes the remainder of the disks. + + +```yaml +variant: fcos +version: 1.3.0 +boot_device: + layout: x86_64 + luks: + tpm2: true + mirror: + devices: + - /dev/sda + - /dev/sdb +storage: + disks: + - device: /dev/sda + partitions: + - label: root-1 + size_mib: 10240 + - label: var-1 + - device: /dev/sdb + partitions: + - label: root-2 + size_mib: 10240 + - label: var-2 + raid: + - name: md-var + level: raid1 + devices: + - /dev/disk/by-partlabel/var-1 + - /dev/disk/by-partlabel/var-2 + filesystems: + - device: /dev/md/md-var + path: /var + format: xfs + wipe_filesystem: true + with_mount_unit: true +``` + +## systemd units + +This example adds a drop-in for the `serial-getty@ttyS0` unit, turning on autologin on `ttyS0` by overriding the `ExecStart=` defined in the default unit. More information on systemd dropins can be found in [the systemd docs][dropins]. + + +```yaml +variant: fcos +version: 1.1.0 +systemd: + units: + - name: serial-getty@ttyS0.service + dropins: + - name: autologin.conf + contents: | + [Service] + TTYVTDisallocate=no + ExecStart= + ExecStart=-/usr/sbin/agetty --autologin core --noclear %I $TERM +``` + +This example creates a new systemd unit called `hello.service`, enables it so it will run on boot, and defines the contents to simply echo `"Hello, World!"`. + + +```yaml +variant: fcos +version: 1.1.0 +systemd: + units: + - name: hello.service + enabled: true + contents: | + [Unit] + Description=A hello world unit! + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=/usr/bin/echo "Hello, World!" + [Install] + WantedBy=multi-user.target +``` + +This example specifies a systemd unit (`example.service`) and a dropin (`proxy.conf`) to be read from local files at transpile time. The file paths are relative to the directory specified with the `--files-dir` command-line option, which must be provided. + + +```yaml +variant: fcos +version: 1.5.0 +systemd: + units: + - name: example.service + contents_local: example.service + - name: rpm-ostreed.service + dropins: + - name: proxy.conf + contents_local: example.conf +``` + +## Podman Quadlets + +This example defines a simple podman quadlet that runs a container. Butane will place the quadlet file in the appropriate directory and Podman's systemd generator will create the corresponding systemd service. + + +```yaml +variant: fcos +version: 1.8.0-experimental +systemd: + quadlets: + - name: sleepy.container + rootful: true + contents: | + [Container] + Image=quay.io/fedora/fedora:latest + Exec=sleep infinity + [Install] + WantedBy=multi-user.target +``` + +## GRUB password + +This example adds a superuser to GRUB and sets a password. Users without the given username and password will not be able to access GRUB command line, modify kernel command-line arguments, or boot non-default OSTree deployments. Password hashes can be generated with `grub2-mkpasswd-pbkdf2`. + + +```yaml +variant: fcos +version: 1.5.0 +grub: + users: + - name: root + password_hash: grub.pbkdf2.sha512.10000.874A958E5264... +``` +## Merging Ignition Configs + +Butane supports merging Ignition configurations directly into Butane configs using different methods. + +This example embeds an Ignition config directly into the Butane config: + + +```yaml +variant: fcos +version: 1.6.0 +ignition: + config: + merge: + - inline: '{"ignition": {"version": "3.5.0"}}' +``` + +This example merges an Ignition from a local file. The file directory can be specified using the `-d/--files` option when running Butane. + + +```yaml +variant: fcos +version: 1.6.0 +ignition: + config: + merge: + - local: ignition.ign +``` + +This example fetches and merges an Ignition config from a remote URL. + + +```yaml +variant: fcos +version: 1.6.0 +ignition: + config: + merge: + - source: https://example.com/sample.ign +``` + +Note that if Butane encounters an Ignition version it does not know about (usually because it is from a newer release), it will show a warning and will continue processing. +This lets you embed newer Ignition versions in Butane configs without blocking on the validation by Butane. + +[spec]: specs.md +[dropins]: https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Description +[fcos-auth-docs]: https://docs.fedoraproject.org/en-US/fedora-coreos/authentication diff --git a/butane/docs/favicon.ico b/butane/docs/favicon.ico new file mode 100644 index 000000000..c715c53ad Binary files /dev/null and b/butane/docs/favicon.ico differ diff --git a/butane/docs/getting-started.md b/butane/docs/getting-started.md new file mode 100644 index 000000000..5d9a9f02c --- /dev/null +++ b/butane/docs/getting-started.md @@ -0,0 +1,110 @@ +--- +nav_order: 2 +--- + +# Getting started + +Butane (formerly the Fedora CoreOS Config Transpiler) is a tool that consumes a Butane Config and produces an Ignition Config, which is a JSON document that can be given to a Fedora CoreOS machine when it first boots. Using this config, a machine can be told to create users, create filesystems, set up the network, install systemd units, and more. + +Butane configs are YAML files conforming to Butane's schema. For more information on the schema, take a look at the [configuration specifications][spec]. + +### Getting Butane + +`butane` can be run from a container image with `podman` or `docker`, installed from Fedora package repositories or downloaded as a standalone binary. + +Using the official container images is the recommended option. + +#### Container image + +This example uses `podman`, but `docker` can also be used. + +```bash +# Pull the container image release +podman pull quay.io/coreos/butane:release + +# Run Butane using standard input and standard output +podman run --interactive --rm quay.io/coreos/butane:release \ + --pretty --strict < your_config.bu > transpiled_config.ign + +# Run Butane using a file as input and standard output +podman run --interactive --rm --security-opt label=disable \ + --volume ${PWD}:/pwd --workdir /pwd quay.io/coreos/butane:release \ + --pretty --strict your_config.bu > transpiled_config.ign +``` + +You may also add the following alias in your shell configuration: + +``` +alias butane='podman run --rm --interactive \ + --security-opt label=disable \ + --volume "${PWD}":/pwd --workdir /pwd \ + quay.io/coreos/butane:release' +``` + +Alternatively you may also create a wrapper script at `~/.local/bin/butane`: + +```bash +#!/bin/sh +exec podman run --rm --interactive \ + --security-opt label=disable \ + --volume "${PWD}":/pwd --workdir /pwd \ + quay.io/coreos/butane:release \ + "${@}" +``` + +Make sure that `~/.local/bin` is in your `$PATH`, or choose another path like `/usr/local/bin`. + +#### Distribution packages + +`butane` is available from the Fedora package repositories: + +``` +$ sudo dnf install -y butane +``` + +#### Standalone binary + +Download the latest version of `butane` and the detached signature from the [releases page](https://github.com/coreos/butane/releases). Verify it with gpg: + +``` +gpg --verify +``` +You may need to download the [Fedora signing keys](https://fedoraproject.org/fedora.gpg) and import them with `gpg --import ` if you have not already done so. + +New releases of `butane` are backwards compatible with old releases, and with the Fedora CoreOS Config Transpiler, unless otherwise noted. + +### Writing and using Butane configs + +As a simple example, let's use `butane` to set the authorized ssh key for the `core` user on a Fedora CoreOS machine. + + +```yaml +variant: fcos +version: 1.1.0 +passwd: + users: + - name: core + ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc... +``` + +In this above file, you'll want to set the `ssh-rsa AAAAB3NzaC1yc...` line to be your ssh public key (which is probably the contents of `~/.ssh/id_rsa.pub`, if you're on Linux). + +If we take this file and give it to `butane`: + +``` +$ ./bin/amd64/butane example.bu + +{"ignition":{"config":{"replace":{"source":null,"verification":{}}},"security":{"tls":{}},"timeouts":{},"version":"3.0.0"},"passwd":{"users":[{"name":"core","sshAuthorizedKeys":["ssh-rsa ssh-rsa AAAAB3NzaC1yc..."]}]},"storage":{},"systemd":{}} +``` + +We can see that it produces a JSON file. This file isn't intended to be human-friendly, and will definitely be a pain to read/edit (especially if you have multi-line things like systemd units). Luckily, you shouldn't have to care about this file! Just provide it to a booting Fedora CoreOS machine and [Ignition][ignition], the utility inside of Fedora CoreOS that receives this file, will know what to do with it. + +The method by which this file is provided to a Fedora CoreOS machine depends on the environment in which the machine is running. For instructions on a given provider, head over to the [list of supported platforms for Ignition][supported-platforms]. + +To see some examples for what else Butane can do, head over to the [examples][examples]. + +[spec]: specs.md +[ignition]: https://coreos.github.io/ignition/ +[supported-platforms]: https://coreos.github.io/ignition/supported-platforms/ +[examples]: examples.md diff --git a/butane/docs/index.md b/butane/docs/index.md new file mode 100644 index 000000000..5e7c30758 --- /dev/null +++ b/butane/docs/index.md @@ -0,0 +1,10 @@ +--- +nav_order: 1 +--- + +# Butane + +Butane (formerly the Fedora CoreOS Config Transpiler, FCCT) translates human readable Butane Configs +into machine readable [Ignition](https://coreos.github.io/ignition/) Configs. See the [getting +started](getting-started.md) guide for how to use Butane and the [configuration specifications](specs.md) +for everything Butane configs support. diff --git a/butane/docs/release-notes.md b/butane/docs/release-notes.md new file mode 100644 index 000000000..da13b1559 --- /dev/null +++ b/butane/docs/release-notes.md @@ -0,0 +1,699 @@ +--- +nav_order: 9 +--- + +# Release notes + +## Upcoming Butane 0.29.0 (unreleased) + +### Breaking changes + +### Features + +### Bug fixes + +### Misc. changes + +- Add support for pretty error reporting, can be controlled through + the use of `--raw-errors` (disable) and `--color`/`--colour` + +### Docs changes + +## Butane 0.28.0 (2026-05-19) + +Starting with this release, Butane binaries are signed with the [Fedora 44 +key](https://getfedora.org/security/). + +### Features + +- Stabilize OpenShift spec 4.22.0, targeting Ignition spec 3.6.0 +- Add OpenShift spec 4.23.0-experimental, targeting Ignition spec + 3.7.0-experimental +- Add `systemd.quadlets` section for embedding Podman Quadlet files + _(fcos 1.8.0-exp, fiot 1.1.0-exp, flatcar 1.2.0-exp, openshift + 4.23.0-exp, r4e 1.2.0-exp)_ + +### Bug fixes + +- Don't warn about partitions being reused by label for boot_device.mirror disks + +### Misc. changes + +- Warn on root partition size is too small _(fcos 1.3.0-1.8.0-exp)_ +- Warn on root partition constrained by another partition _(fcos 1.3.0-1.8.0-exp)_ + +## Butane 0.27.0 (2026-02-27) + +### Features + +- Stabilize Fcos spec 1.7.0, targeting Ignition spec 3.6.0 +- Add Fcos spec 1.8.0-experimental, targeting Ignition spec 3.7.0-experimental +- Update Flatcar spec 1.2.0-experimental to target Ignition spec 3.7.0-experimental +- Update Fiot spec 1.1.0-experimental to target Ignition spec 3.7.0-experimental +- Update R4E spec 1.2.0-experimental to target Ignition spec 3.7.0-experimental +- Update OpenShift spec 4.22.0-experimental to target Ignition spec 3.7.0-experimental + +### Docs changes + +- Re-vendor latest ignition release; 3.6.0-experimental becomes 3.6.0 + +## Butane 0.26.0 (2026-01-16) + +Starting with this release, Butane binaries are signed with the [Fedora 43 +key](https://getfedora.org/security/). + +### Breaking changes + +- Require `boot_device.layout` when using `boot_device.mirror` _(fcos 1.7.0-exp)_ + +### Features + +- Stabilize OpenShift spec 4.21.0, targeting Ignition spec 3.5.0 +- Add OpenShift spec 4.22.0-experimental, targeting Ignition spec + 3.6.0-experimental +- Add support for mode and ownership settings for trees. + +### Bug fixes + +- Warn for `boot_device.layout` to be specified when using `boot_device.mirror` _(fcos 1.3.0-1.6.0)_ + +### Docs changes + +- Update `boot_device.mirror` examples to specify `boot_device.layout` + +## Butane 0.25.1 (2025-09-24) + +### Docs changes + +- Update docs around the use of setuid/gid from Ignition [bug](coreos/ignition#2042)' + +### Misc. changes + +- Update vendor'd Ignition dependency to point to latest v2.23.0 + +## Butane 0.25.0 (2025-09-08) + +### Features + +- Stabilize OpenShift spec 4.20.0, targeting Ignition spec 3.5.0 +- Add OpenShift spec 4.21.0-experimental, targeting Ignition spec + 3.6.0-experimental + +### Bug fixes + +- Stop overriding default LUKS cipher algorithm in FIPS mode _(openshift 4.20.0)_ + +### Docs changes + +- Add missing examples in upgrading-openshift _(openshift 4.14)_ + +## Butane 0.24.0 (2025-05-27) + +### Features + +- Validate merged/replaced Ignition configs if they are local/inline _(all base specifications)_ +- Stabilize OpenShift spec 4.19.0, targeting Ignition spec 3.5.0 +- Add OpenShift spec 4.20.0-experimental, targeting Ignition spec + 3.6.0-experimental +- Add TMT test support with initial smoke test + +### Bug fixes + +- Fail if LUKS method is not specified while `boot_device.luks.device` is set _(fcos 1.7.0-exp)_ +- Validate kernel arguments when CEX support is enabled on s390x _(4.19.0 and 4.20.0)_ + +### Misc. changes + +- Roll back to Ignition spec 3.5.0 _(openshift 4.19.0)_ + +## Butane 0.23.0 (2024-12-03) + +Starting with this release, Butane binaries are signed with the [Fedora 41 +key](https://getfedora.org/security/). + +### Features + +- Add OpenShift spec 4.19.0-experimental, targeting Ignition spec + 3.6.0-experimental +- Stabilize OpenShift spec 4.18.0, targeting Ignition spec 3.4.0 +- Stabilize Fcos spec 1.6.0, targeting Ignition spec 3.5.0 +- Add Fcos spec 1.7.0-experimental, targeting Ignition spec + 3.6.0-experimental +- Update Fiot spec 1.1.0-experimental to target Ignition spec + 3.6.0-experimental +- Update Flatcar spec 1.2.0-experimental to target Ignition spec + 3.6.0-experimental +- Update OpenShift spec 4.18.0-experimental, targeting Ignition spec + 3.6.0-experimental +- Update R4e spec 1.2.0-experimental to target Ignition spec + 3.6.0-experimental +- Support LUKS encryption using IBM CEX secure keys on s390x _(fcos 1.6)_ _(openshift 4.18.0-exp)_ + +### Docs changes + +- Re-vendor latest ignition release; 3.5.0-experimental becomes 3.5.0 + +## Butane 0.22.0 (2024-09-20) + +### Features + +- Stabilize OpenShift spec 4.17.0, targeting Ignition spec 3.4.0 +- Add OpenShift spec 4.18.0-experimental, targeting Ignition spec + 3.5.0-experimental +- Support and documentation for `grub` section moved to OpenShift + 4.18.0-experimental spec. + +### Misc. changes + +- Roll back to Ignition spec 3.4.0 _(openshift 4.17.0)_ + +## Butane 0.21.0 (2024-06-06) + +Starting with this release, Butane binaries are signed with the [Fedora 40 +key](https://getfedora.org/security/). + +### Features + +- Support `storage.luks.clevis` (flatcar 1.2.0-exp) +- Stabilize OpenShift spec 4.16.0, targeting Ignition spec 3.4.0 +- Add OpenShift spec 4.17.0-experimental, targeting Ignition spec + 3.5.0-experimental + +## Butane 0.20.0 (2024-02-19) + +Starting with this release, Butane binaries are signed with the [Fedora 39 +key](https://getfedora.org/security/). + +### Features + +- Support s390x layouts in `boot_device` section (fcos 1.6.0-exp, openshift 4.16.0-exp) +- Stabilize OpenShift spec 4.15.0, targeting Ignition spec 3.4.0 +- Add OpenShift spec 4.16.0-experimental, targeting Ignition spec + 3.5.0-experimental + +### Misc. changes + +- Require Go 1.20+ + +## Butane 0.19.0 (2023-10-03) + +Starting with this release, Butane binaries are signed with the [Fedora 38 +key](https://getfedora.org/security/). + +### Breaking changes + +- Spec implementations require a `FieldFilters()` method (Go API) +- Reports from `Unvalidated` functions can now include `json` paths (Go API) + +### Features + +- Add `-c`/`--check` option to check config without producing output +- Warn if config attempts to reuse partition by label _(fcos 1.6.0-exp, + openshift 4.14.0)_ +- Require `storage.filesystems.path` to start with `/etc` or `/var` if + `with_mount_unit` is true _(fcos 1.6.0-exp, openshift 4.14.0)_ +- Stabilize OpenShift spec 4.14.0, targeting Ignition spec 3.4.0 +- Add OpenShift spec 4.15.0-experimental, targeting Ignition spec + 3.5.0-experimental +- Add new variant `fiot` for fedora-iot + +### Bug fixes + +- Fix line/column reporting for `http_headers` errors +- Fix line/column reporting for unsupported field errors _(r4e)_ + +### Misc. changes + +- Add error structs for YAML unmarshal errors, unknown config versions (Go API) +- Roll back to Ignition spec 3.4.0 _(openshift 4.14.0)_ + +### Docs changes + +- Document consequence of setting `systemd.units.mask` to false +- Document `grub` section _(openshift 4.15.0-exp)_ +- Document `/dev/disk/by-id/coreos-boot-disk` _(fcos, openshift 4.11.0+)_ +- Don't claim to support generating swap units _(openshift 4.8.0 - 4.13.0)_ +- Document `key_file` `compression` field _(openshift 4.8.0 - 4.9.0)_ +- Document support for special mode bits and `arn` URLs _(r4e 1.1.0+)_ +- Improve rendering of spec docs on docs site + +## Butane 0.18.0 (2023-03-24) + +### Breaking changes + +- Remove deprecated `rhcos` variant + +### Features + +- Support offline Tang provisioning via pre-shared advertisement _(fcos 1.5.0+, + openshift 4.14.0-exp)_ +- Support local file embedding for SSH keys and systemd units _(fcos 1.5.0+, + flatcar 1.1.0+, openshift 4.14.0-exp, r4e 1.1.0+)_ +- Allow enabling discard passthrough on LUKS devices _(fcos 1.5.0+, + flatcar 1.1.0+, openshift 4.14.0-exp)_ +- Allow specifying arbitrary LUKS open options _(fcos 1.5.0+, + flatcar 1.1.0+, openshift 4.14.0-exp)_ +- Allow specifying user password hash _(openshift 4.13.0+)_ +- Stabilize Fedora CoreOS spec 1.5.0, targeting Ignition spec 3.4.0 +- Stabilize Flatcar spec 1.1.0, targeting Ignition spec 3.4.0 +- Stabilize OpenShift spec 4.13.0, targeting Ignition spec 3.2.0 +- Stabilize RHEL for Edge spec 1.1.0, targeting Ignition spec 3.4.0 +- Add Fedora CoreOS spec 1.6.0-experimental, targeting Ignition spec + 3.5.0-experimental +- Add Flatcar spec 1.2.0-experimental, targeting Ignition spec + 3.5.0-experimental +- Add OpenShift spec 4.14.0-experimental, targeting Ignition spec + 3.5.0-experimental +- Add RHEL for Edge spec 1.2.0-experimental, targeting Ignition spec + 3.5.0-experimental + +### Bug fixes + +- Use systemd default dependencies in mount units for Tang-backed LUKS volumes +- Allow setting `storage.trees.local` to the `--files-dir` directory + +### Misc. changes + +- Roll back to Ignition spec 3.2.0 _(openshift 4.13.0)_ +- Drop `extensions` section _(fcos 1.5.0+, openshift 4.13.0+)_ +- Drop `LuksOption` and `RaidOption` types _(Go API for fcos 1.5.0+, + flatcar 1.1.0+, openshift 4.14.0-experimental)_ +- Require Go 1.18+ + +### Docs changes + +- Document that `hash` fields describe decompressed data +- Clarify spec docs for `files`/`luks` `hash` fields +- Document SSH key file path used by OpenShift 4.13+ _(openshift)_ +- Document command to generate GRUB password hashes + +## Butane 0.17.0 (2023-01-04) + +Starting with this release, Butane binaries are signed with the [Fedora 37 +key](https://getfedora.org/security/). + +### Features + +- Add RHEL for Edge (`r4e`) spec 1.0.0 and 1.1.0-experimental, targeting + Ignition spec 3.3.0 and 3.4.0-experimental respectively + +### Bug fixes + +- Fix version string in release container + +## Butane 0.16.0 (2022-10-14) + +### Features + +- Stabilize OpenShift spec 4.12.0, targeting Ignition spec 3.2.0 +- Add OpenShift spec 4.13.0-experimental, targeting Ignition spec + 3.4.0-experimental +- Ship aarch64 macOS binary in GitHub release artifacts + +### Misc. changes + +- Roll back to Ignition spec 3.2.0 _(openshift 4.12.0)_ +- Require Go 1.17+ +- test: Check docs on macOS and Windows if dependencies are available + +### Docs changes + +- Document `passwd.users.should_exist` and `passwd.groups.should_exist` fields + _(fcos 1.2.0+, flatcar, rhcos)_ +- Clarify spec docs for `files`/`directories`/`links` `group` fields +- Document that `user`/`group` fields aren't applied to hard links + +## Butane 0.15.0 (2022-06-23) + +Starting with this release, Butane binaries are signed with the [Fedora 36 +key](https://getfedora.org/security/). + +### Breaking changes + +- Return selected `compression` field value from `MakeDataURL()` _(Go API)_ + +### Features + +- Add Flatcar spec 1.0.0 and 1.1.0-experimental, targeting Ignition spec + 3.3.0 and 3.4.0-experimental respectively +- Stabilize OpenShift spec 4.11.0, targeting Ignition spec 3.2.0 +- Add OpenShift spec 4.12.0-experimental, targeting Ignition spec + 3.4.0-experimental +- Add arm64 support to container +- Add GRUB password support _(fcos 1.5.0-exp, openshift 4.12.0-exp)_ +- Add `TranslationSet` `AddFromCommonObject()` and `Map()` methods _(Go API)_ + +### Bug fixes + +- Set `compression` field for uncompressed `inline`/`local` resources, fixing + provisioning failure when merged with a compressed parent resource +- Fix local file inclusion on Windows +- Fix `build` script on Windows + +### Misc. changes + +- Derive container from Fedora image to support use in multi-stage builds +- Fail if setuid/setgid/sticky mode bits specified _(openshift 4.10.0+)_ +- Update to Ignition 2.14.0 +- Roll back to Ignition spec 3.2.0 _(openshift 4.11.0)_ + +### Docs changes + +- Support `arn` URL scheme _(fcos 1.5.0-exp, openshift 4.12.0-exp)_ +- Document support status of setuid/setgid/sticky mode bits in each spec +- Document support for `gs` URLs _(openshift 4.8.0+)_ +- Document support for `compression` field _(openshift 4.8.0 - 4.9.0)_ +- Correctly document supported URL schemes _(openshift 4.10.0)_ +- examples: Use containerized `mkpasswd` +- Convert `NEWS` to Markdown and move to docs site + +## Butane 0.14.0 (2022-01-27) + +Starting with this release, Butane binaries are signed with the [Fedora 35 +key](https://getfedora.org/security/). + +### Breaking changes + +- Drop `TranslateBytesOptions.Strict` field; callers should fail on + non-empty reports instead _(Go API)_ + +### Features + +- Stabilize OpenShift spec 4.10.0, targeting Ignition spec 3.2.0 +- Add OpenShift spec 4.11.0-experimental, targeting Ignition spec + 3.4.0-experimental +- Warn on incorrect partition numbers for reserved labels _(fcos, openshift)_ +- Require `storage.files.contents.source` URLs to use `data` scheme + _(openshift)_ +- Re-enable automatic and manual resource compression _(openshift 4.10.0+)_ +- Add `extensions` section _(fcos 1.5.0-exp, openshift 4.11.0-exp)_ + +### Bug fixes + +- Correctly fail on validation warnings if `--strict` is specified +- Statically link official Linux binaries + +### Misc. changes + +- Roll back to Ignition spec 3.2.0 _(openshift 4.10.0)_ +- Add deprecation warning for `rhcos` variant +- Add reserved partitions to `aarch64`/`ppc64le` `boot_device.mirror` layouts + _(fcos, openshift)_ + +### Docs changes + +- Improve getting-started instructions for running in container +- Document availability of `gs` URL scheme +- Correctly document availability of `compression` fields +- Correctly document `ignition` section as optional +- Add `with_mount_unit` `swap` support to migration guide _(fcos 1.4.0)_ +- Document build process and contribution flow for release binaries + +## Butane 0.13.1 (2021-08-04) + +### Misc. changes + +- Roll back to Ignition spec 3.2.0, since 3.3.0 support didn't make + it into OpenShift 4.9. No 3.3.0 features were permitted in this + config version, so this shouldn't break configs. _(openshift 4.9.0)_ +- Send `--help` output to stdout +- Drop support for Go 1.13 and 1.14 + +### Docs changes + +- Correctly snake-case `ignition.proxy` fields + +## Butane 0.13.0 (2021-07-13) + +### Features + +- Stabilize Fedora CoreOS spec 1.4.0, targeting Ignition spec 3.3.0 +- Add Fedora CoreOS spec 1.5.0-experimental, targeting Ignition spec + 3.4.0-experimental +- Stabilize OpenShift spec 4.9.0, targeting Ignition spec 3.3.0 +- Add OpenShift spec 4.10.0-experimental, targeting Ignition spec + 3.4.0-experimental +- Support `none` filesystem format _(fcos 1.4.0+)_ + +### Bug fixes + +- Correctly track input line/column in `kernel_arguments` section + +### Misc. changes + +- Deprecate `rhcos` 0.1.0 spec in favor of `openshift` variant +- Disable `kernel_arguments` section in favor of `openshift.kernel_arguments` + _(openshift 4.9.0+)_ +- Convert `ClevisCustom.Config`, `ClevisCustom.Pin`, `Link.Target`, and + `Raid.Level` Go fields to pointers _(fcos 1.4.0+, openshift 4.9.0+)_ + +### Docs changes + +- Document default value for Clevis `threshold` + +## Butane 0.12.1 (2021-06-10) + +### Bug fixes + +- Disable automatic resource compression _(openshift 4.8.0, + openshift 4.9.0-exp)_ + +### Misc. changes + +- Fail if file compression specified _(openshift 4.8.0, openshift 4.9.0-exp)_ + +## Butane 0.12.0 (2021-06-08) + +Starting with this release, Butane binaries are signed with the [Fedora 34 +key](https://getfedora.org/security/). + +### Features + +- Add `kernel_arguments` section _(fcos 1.4.0-exp, openshift 4.9.0-exp)_ + +### Bug fixes + +- Fix incorrect config paths in validation reports on 386 architecture + +### Misc. changes + +- Fail on `btrfs` filesystem format _(openshift 4.8.0, openshift 4.9.0-exp)_ +- Add comment to MachineConfig output noting that the config is + machine-generated + +## Butane 0.11.0 (2021-04-05) + +### Breaking changes + +- Rename project to Butane and binary to `butane` +- Change package path to `github.com/coreos/butane` _(Go API)_ +- Remove `translate.AddIdentity()` in favor of `translate.MergeP()` _(Go API)_ + +### Features + +- Add OpenShift spec 4.8.0, targeting Ignition spec 3.2.0 +- Output MachineConfig unless `-r`/`--raw` specified _(openshift 4.8.0)_ +- Error on Ignition fields discouraged by OpenShift _(openshift 4.8.0)_ +- Add `metadata` section for MachineConfig metadata _(openshift 4.8.0)_ +- Add `openshift` section for MachineConfig configuration _(openshift 4.8.0)_ +- Set appropriate LUKS cipher if `openshift.fips` enabled _(openshift 4.8.0)_ +- Add OpenShift spec 4.9.0-experimental, targeting Ignition spec + 3.3.0-experimental + +### Misc. changes + +- Remove RHEL CoreOS spec 0.2.0-experimental +- Refactor translation tracking for report entries +- Add undocumented `-D`/`--debug` option to report translation map + +### Docs changes + +- Provide separate config upgrade guide for each variant +- Document `storage.filesystems.resize` +- Fix filesystem resize example in upgrade docs +- Document default for `storage.filesystems.wipe_filesystem` + +## FCCT 0.10.0 (2021-02-01) + +### Features + +- Create systemd `swap` unit when `with_mount_unit` is enabled on swap area + _(fcos 1.4.0-exp, rhcos 0.2.0-exp)_ + +### Bug fixes + +- Drop erroneous EFI partition in `boot_device.mirror` `ppc64le` layout +- Fix panic translating `boot_device` when config is invalid + +## FCCT 0.9.0 (2021-01-05) + +### Bug fixes + +- Avoid ESP RAID desynchronization by creating independent ESP filesystems + +### Docs changes + +- Clarify semantics of `systemd.units.name` +- Correctly document `storage.filesystems.path` as optional +- Fix nesting of `storage.luks` and `storage.trees` sections +- Move codebase layout info from README to developer docs +- Recommend container image or distro package over standalone binary + +## FCCT 0.8.0 (2020-12-04) + +Starting with this release, Butane binaries are signed with the [Fedora 33 +key](https://getfedora.org/security/). + +### Breaking changes + +- Restructure Go API + +### Features + +- Stabilize Fedora CoreOS spec 1.3.0, targeting Ignition spec 3.2.0 +- Add Fedora CoreOS spec 1.4.0-experimental, targeting Ignition spec + 3.3.0-experimental +- Add RHEL CoreOS spec 0.1.0, targeting Ignition spec 3.2.0 +- Add RHEL CoreOS spec 0.2.0-experimental, targeting Ignition spec + 3.3.0-experimental +- Add `boot_device` section for configuring boot device LUKS and mirroring + _(fcos 1.3.0, rhcos 0.1.0)_ + +### Bug fixes + +- Fix `systemd-fsck@.service` dependencies in generated mount units + +### Misc. changes + +- Warn if file/dir modes appear to have been specified in decimal +- Validate input in translation functions taking Go structs _(Go API)_ +- Allow registering external translators _(Go API)_ +- Allow specs to derive from other specs _(Go API)_ + +### Docs changes + +- Document Clevis `custom` and LUKS `wipe_volume` fields +- Add LUKS and mirroring examples +- Add password authentication example + +## FCCT 0.7.0 (2020-10-23) + +### Features + +- Stabilize FCC spec 1.2.0, targeting Ignition spec 3.2.0 +- Add FCC spec 1.3.0-experimental, targeting Ignition spec + 3.3.0-experimental +- Add `storage.luks` section for creating LUKS2 encrypted volumes + _(1.2.0)_ +- Add `resize` field for modifying partition size _(1.2.0)_ +- Add `should_exist` field for deleting users & groups _(1.2.0)_ +- Add `NoResourceAutoCompression` translate option to skip automatic + compression _(Go API)_ + +### Docs changes + +- Switch to GitHub Pages + +## FCCT 0.6.0 (2020-05-28) + +Starting with this release, Butane binaries are signed with the [Fedora 32 +key](https://getfedora.org/security/). + +### Features + +- Stabilize FCC spec 1.1.0, targeting Ignition spec 3.1.0 +- Add FCC spec 1.2.0-experimental, targeting Ignition spec + 3.2.0-experimental +- Add `inline` field to TLS certificate authorities and config merge and + replace _(1.1.0)_ +- Add `local` field for embedding contents from local file _(1.1.0)_ +- Add `storage.trees` section for embedding local directory trees _(1.1.0)_ +- Auto-select smallest encoding for `inline` or `local` contents _(1.1.0)_ +- Add `http_headers` field for specifying HTTP headers on fetch _(1.1.0)_ + +### Bug fixes + +- Include mount options in generated mount units _(1.1.0)_ +- Validate uniqueness constraints within FCC sections +- Omit empty values from output JSON +- Append newline to output + +### Docs changes + +- Document support for CA bundles in Ignition >= 2.3.0 +- Document support for `sha256` resource verification _(1.1.0)_ +- Clarify semantics of `overwrite` and `mode` fields + +## FCCT 0.5.0 (2020-03-23) + +### Breaking changes + +- Previously, command-line options could be preceded by a single dash + (`-strict`) or double dash (`--strict`). Accept only the double-dash form. + +### Features + +- Accept input filename directly on command line, without `--input` +- Add short equivalents of command-line options + +### Bug fixes + +- Fail if unexpected non-option arguments are specified + +### Misc. changes + +- Deprecate `--input` and hide it from `--help` +- Document `files[].append[].inline` property +- Update docs for switch to Fedora signing keys + +## FCCT 0.4.0 (2020-01-24) + +### Features + +- Add `mount_options` field to filesystem entry + +### Misc. changes + +- Add `release` tag to container of latest release +- Vendor dependencies + +## FCCT 0.3.0 (2020-01-23) + +### Features + +- Add v1.1.0-experimental spec +- Add `with_mount_unit` field to generate mount unit from filesystem entry + +### Bug fixes + +- Report warnings and errors to stderr, not stdout +- Truncate output file before writing +- Fix line and column reporting + +### Misc. changes + +- Document syntax of inline file contents +- Document usage of published container image + +## FCCT 0.2.0 (2019-07-24) + +### Features + +- Add `--version` flag +- Add Dockerfile and build containers automatically on quay.io + +### Bug fixes + +- Fix validation of paths for files and directories +- Fix `--output` flag handling + +### Misc. changes + +- Add tests for the examples in the docs +- Add travis integration + +## FCCT 0.1.0 (2019-07-10) + +Initial Release of FCCT. While the golang API is not stable, the Fedora CoreOS +Configuration language is. Configs written with version 1.0.0 will continue to +work with future releases of FCCT. diff --git a/butane/docs/specs.md b/butane/docs/specs.md new file mode 100644 index 000000000..a303ae733 --- /dev/null +++ b/butane/docs/specs.md @@ -0,0 +1,104 @@ +--- +has_children: true +nav_order: 4 +has_toc: false +--- + +# Configuration specifications + +Butane Configs must conform to a specific variant and version of the Butane schema, specified with the `variant` and `version` fields in the configuration. + +See the [Upgrading Configs](upgrading.md) page for instructions to update a configuration to the latest specification. + +## Stable specification versions + +We recommend that you always use the latest **stable** specification for your operating system to benefit from new features and bug fixes. The following **stable** specification versions are currently supported in Butane: + +- Fedora CoreOS (`fcos`) + - [v1.7.0](config-fcos-v1_7.md) + - [v1.6.0](config-fcos-v1_6.md) + - [v1.5.0](config-fcos-v1_5.md) + - [v1.4.0](config-fcos-v1_4.md) + - [v1.3.0](config-fcos-v1_3.md) + - [v1.2.0](config-fcos-v1_2.md) + - [v1.1.0](config-fcos-v1_1.md) + - [v1.0.0](config-fcos-v1_0.md) +- Flatcar (`flatcar`) + - [v1.1.0](config-flatcar-v1_1.md) + - [v1.0.0](config-flatcar-v1_0.md) +- OpenShift (`openshift`) + - [v4.22.0](config-openshift-v4_22.md) + - [v4.21.0](config-openshift-v4_21.md) + - [v4.20.0](config-openshift-v4_20.md) + - [v4.19.0](config-openshift-v4_19.md) + - [v4.18.0](config-openshift-v4_18.md) + - [v4.17.0](config-openshift-v4_17.md) + - [v4.16.0](config-openshift-v4_16.md) + - [v4.15.0](config-openshift-v4_15.md) + - [v4.14.0](config-openshift-v4_14.md) + - [v4.13.0](config-openshift-v4_13.md) + - [v4.12.0](config-openshift-v4_12.md) + - [v4.11.0](config-openshift-v4_11.md) + - [v4.10.0](config-openshift-v4_10.md) + - [v4.9.0](config-openshift-v4_9.md) + - [v4.8.0](config-openshift-v4_8.md) +- RHEL for Edge (`r4e`) + - [v1.1.0](config-r4e-v1_1.md) + - [v1.0.0](config-r4e-v1_0.md) +- Fedora IoT (`fiot`) + - [v1.0.0](config-fiot-v1_0.md) + +## Experimental specification versions + +Do not use **experimental** specifications for anything beyond **development and testing** as they are subject to change **without warning or announcement**. The following **experimental** specification versions are currently available in Butane: + +- Fedora CoreOS (`fcos`) + - [v1.8.0-experimental](config-fcos-v1_8-exp.md) +- Flatcar (`flatcar`) + - [v1.2.0-experimental](config-flatcar-v1_2-exp.md) +- OpenShift (`openshift`) + - [v4.23.0-experimental](config-openshift-v4_23-exp.md) +- RHEL for Edge (`r4e`) + - [v1.2.0-experimental](config-r4e-v1_2-exp.md) +- Fedora IoT (`fiot`) + - [v1.1.0-experimental](config-fiot-v1_1-exp.md) + +## Butane specifications and Ignition specifications + +Each version of the Butane specification corresponds to a version of the Ignition specification: + +| Butane variant | Butane spec | Ignition spec | +|----------------|---------------------|--------------------| +| `fcos` | 1.0.0 | 3.0.0 | +| `fcos` | 1.1.0 | 3.1.0 | +| `fcos` | 1.2.0 | 3.2.0 | +| `fcos` | 1.3.0 | 3.2.0 | +| `fcos` | 1.4.0 | 3.3.0 | +| `fcos` | 1.5.0 | 3.4.0 | +| `fcos` | 1.6.0 | 3.5.0 | +| `fcos` | 1.7.0 | 3.6.0 | +| `fcos` | 1.8.0-experimental | 3.7.0-experimental | +| `flatcar` | 1.0.0 | 3.3.0 | +| `flatcar` | 1.1.0 | 3.4.0 | +| `flatcar` | 1.2.0-experimental | 3.7.0-experimental | +| `openshift` | 4.8.0 | 3.2.0 | +| `openshift` | 4.9.0 | 3.2.0 | +| `openshift` | 4.10.0 | 3.2.0 | +| `openshift` | 4.11.0 | 3.2.0 | +| `openshift` | 4.12.0 | 3.2.0 | +| `openshift` | 4.13.0 | 3.2.0 | +| `openshift` | 4.14.0 | 3.4.0 | +| `openshift` | 4.15.0 | 3.4.0 | +| `openshift` | 4.16.0 | 3.4.0 | +| `openshift` | 4.17.0 | 3.4.0 | +| `openshift` | 4.18.0 | 3.4.0 | +| `openshift` | 4.19.0 | 3.5.0 | +| `openshift` | 4.20.0 | 3.5.0 | +| `openshift` | 4.21.0 | 3.5.0 | +| `openshift` | 4.22.0 | 3.6.0 | +| `openshift` | 4.23.0-experimental | 3.7.0-experimental | +| `r4e` | 1.0.0 | 3.3.0 | +| `r4e` | 1.1.0 | 3.4.0 | +| `r4e` | 1.2.0-experimental | 3.7.0-experimental | +| `fiot` | 1.0.0 | 3.4.0 | +| `fiot` | 1.1.0-experimental | 3.7.0-experimental | diff --git a/butane/docs/upgrading-fcos.md b/butane/docs/upgrading-fcos.md new file mode 100644 index 000000000..768be2616 --- /dev/null +++ b/butane/docs/upgrading-fcos.md @@ -0,0 +1,620 @@ +--- +title: Fedora CoreOS +parent: Upgrading configs +nav_order: 1 +--- + +# Upgrading Fedora CoreOS configs + +Occasionally, changes are made to Fedora CoreOS Butane configs (those that specify `variant: fcos`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} + +## From Version 1.6.0 to Version 1.7.0 + +There are no breaking changes between versions 1.6.0 and 1.7.0 of the `fcos` configuration specification. Any valid 1.6.0 configuration can be updated to a 1.7.0 configuration by changing the version string in the config. + +## From Version 1.5.0 to Version 1.6.0 + +There are no breaking changes between versions 1.5.0 and 1.6.0 of the `fcos` configuration specification. Any valid 1.5.0 configuration can be updated to a 1.6.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + + +### LUKS CEX support + +The `luks` sections in `storage` and `boot_device` gained a `cex` field. If enabled, this will configure an encrypted root filesystem on a s390x system using IBM Crypto Express (CEX) card. + + +```yaml +variant: fcos +version: 1.6.0 +kernel_arguments: + should_exist: + - rd.luks.key=/etc/luks/cex.key +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + cex: + enabled: true +``` + +### Boot_Device Layouts s390x support + +The `boot_device` section gained support for the following layouts `s390x-eckd`, `s390x-zfcp`, `s390x-virt`. This enables the use of the `boot_device` sugar for s390x systems. + +The `s390x-eckd` layout enables configuration of an encrypted root filesystem for a DASD device. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +The `s390x-zfcp` layout enables configuration of an encrypted root filesystem for a zFCP device. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-zfcp + luks: + device: /dev/sdb + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +The `s390x-virt` layout enables configuration of an encrypted root filesystem for KVM. + + +```yaml +variant: fcos +version: 1.6.0 +boot_device: + layout: s390x-virt + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +## From Version 1.4.0 to Version 1.5.0 + +There are no breaking changes between versions 1.4.0 and 1.5.0 of the `fcos` configuration specification. Any valid 1.4.0 configuration can be updated to a 1.5.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### GRUB passwords + +The config gained a new top-level `grub` section. It contains a `users` section with a list of usernames and corresponding password hashes for authenticating to the GRUB bootloader. If any users are specified, GRUB will require authentication before using the GRUB command line, modifying kernel command-line arguments, or booting non-default OSTree deployments. Password hashes can be generated with `grub2-mkpasswd-pbkdf2`. + + +```yaml +variant: fcos +version: 1.5.0 +grub: + users: + - name: admin + password_hash: grub.pbkdf2.sha512.10000.874A958E5264... +``` + +### Offline Tang provisioning + +The `tang` sections in `storage.luks` and `boot_device.luks` gained a new `advertisement` field. If specified, Ignition will use it to provision the Tang server binding rather than fetching the advertisement from the server at runtime. This allows the server to be unavailable at provisioning time. The advertisement can be obtained from the server with `curl http://tang.example.com/adv`. + + +```yaml +variant: fcos +version: 1.5.0 +boot_device: + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + advertisement: "{\"payload\": \"...\", \"protected\": \"...\", \"signature\": \"...\"}" +storage: + luks: + - name: luks-tang + device: /dev/sdb + clevis: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + advertisement: "{\"payload\": \"...\", \"protected\": \"...\", \"signature\": \"...\"}" +``` + +### LUKS discard + +The `luks` sections in `storage` and `boot_device` gained a new `discard` field. If specified and true, the LUKS volume will issue discard commands to the underlying block device when blocks are freed. This improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. + + +```yaml +variant: fcos +version: 1.5.0 +boot_device: + luks: + discard: true + tpm2: true +storage: + luks: + - name: luks-tpm + device: /dev/sdb + discard: true + clevis: + tpm2: true +``` + +### LUKS open options + +The `storage.luks` section gained a new `open_options` field. It is a list of options Ignition should pass to `cryptsetup luksOpen` when unlocking the volume. Ignition also passes `--persistent`, so any options that support persistence will be saved to the volume and automatically used for future unlocks. Any options that do not support persistence will only be applied to Ignition's initial unlock of the volume. + + +```yaml +variant: fcos +version: 1.5.0 +storage: + luks: + - name: luks-tpm + device: /dev/sdb + open_options: + - "--perf-no_read_workqueue" + - "--perf-no_write_workqueue" + clevis: + tpm2: true +``` + +### AWS S3 access point ARN support + +The sections which allow fetching a remote URL now accept AWS S3 access point ARNs (`arn:aws:s3:::accesspoint//object/`) in the `source` field. + + +```yaml +variant: fcos +version: 1.5.0 +storage: + files: + - path: /etc/example + mode: 0644 + contents: + source: arn:aws:s3:us-west-1:123456789012:accesspoint/test/object/some/path +``` + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: fcos +version: 1.5.0 +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +## From Version 1.3.0 to 1.4.0 + +There are no breaking changes between versions 1.3.0 and 1.4.0 of the `fcos` configuration specification. Any valid 1.3.0 configuration can be updated to a 1.4.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### Kernel arguments + +The config gained a new top-level `kernel_arguments` section. It allows specifying arguments that should be included or excluded from the kernel command line. + + +```yaml +variant: fcos +version: 1.4.0 +kernel_arguments: + should_exist: + - foobar + - baz boo + should_not_exist: + - raboof +``` + +The config above will ensure that the kernel argument `foobar` is present, and the kernel argument `raboof` is absent. It will also ensure that the kernel arguments `baz boo` are present exactly in that order. + +### New filesystem format `none` + +The `format` field of the `filesystems` section can now be set to `none`. This setting erases an existing filesystem signature without creating a new filesystem (if `wipe_filesystem` is true), or fails if there is any existing filesystem (if `wipe_filesystem` is false). + + +```yaml +variant: fcos +version: 1.4.0 +storage: + filesystems: + - device: /dev/vdb1 + wipe_filesystem: true + format: none +``` + +Refer to the [Ignition filesystem reuse semantics](https://coreos.github.io/ignition/operator-notes/#filesystem-reuse-semantics) for more information. + +### Automatic generation of systemd swap units + +The `with_mount_unit` field of the `filesystems` section can now be set to `true` if the `format` field is set to `swap`. Butane will generate a systemd swap unit for the specified swap area. + + +```yaml +variant: fcos +version: 1.4.0 +storage: + filesystems: + - device: /dev/vdb1 + format: swap + wipe_filesystem: true + with_mount_unit: true +``` + +## From Version 1.2.0 to 1.3.0 + +There are no breaking changes between versions 1.2.0 and 1.3.0 of the `fcos` configuration specification. Any valid 1.2.0 configuration can be updated to a 1.3.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### Boot disk mirroring and LUKS + +The config gained a new top-level `boot_device` section with `luks` and `mirror` subsections, which provide a simple way to configure encryption and/or mirroring for the boot disk. When `luks` is specified, the root filesystem is encrypted and can be unlocked with any combination of a TPM2 device and network Tang servers. When `mirror` is specified, all default partitions are replicated across multiple disks, allowing the system to survive disk failure. On aarch64 or ppc64le systems, the `layout` field must be set to `aarch64` or `ppc64le` to select the correct partition layout. + + +```yaml +variant: fcos +version: 1.3.0 +boot_device: + layout: ppc64le + mirror: + devices: + - /dev/sda + - /dev/sdb + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + tpm2: true + threshold: 2 +``` + +## From Version 1.1.0 to 1.2.0 + +There are no breaking changes between versions 1.1.0 and 1.2.0 of the `fcos` configuration specification. Any valid 1.1.0 configuration can be updated to a 1.2.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### Partition resizing + +The `partition` section gained a new `resize` field. When true, Ignition will resize an existing partition if it matches the config in all respects except the partition size. + + +```yaml +variant: fcos +version: 1.2.0 +storage: + disks: + - device: /dev/sda + partitions: + - number: 4 + label: root + size_mib: 16384 + resize: true +``` + +### LUKS encrypted storage + +Ignition now supports creating LUKS2 encrypted storage volumes. Volumes can be configured to allow unlocking with any combination of a TPM2 device via Clevis, network Tang servers via Clevis, and static key files. Alternatively, the Clevis configuration can be manually specified with a custom PIN and CFG. If a key file is not specified for a device, an ephemeral one will be created. + + +```yaml +variant: fcos +version: 1.2.0 +storage: + luks: + - name: static-key-example + device: /dev/sdb + key_file: + inline: REPLACE-THIS-WITH-YOUR-KEY-MATERIAL + - name: tpm-example + device: /dev/sdc + clevis: + tpm2: true + - name: tang-example + device: /dev/sdd + clevis: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + filesystems: + - path: /var/lib/static_key_example + device: /dev/disk/by-id/dm-name-static-key-example + format: ext4 + label: STATIC-EXAMPLE + with_mount_unit: true + - path: /var/lib/tpm_example + device: /dev/disk/by-id/dm-name-tpm-example + format: ext4 + label: TPM-EXAMPLE + with_mount_unit: true + - path: /var/lib/tang_example + device: /dev/disk/by-id/dm-name-tang-example + format: ext4 + label: TANG-EXAMPLE + with_mount_unit: true +``` + +### User/group deletion + +The `passwd` `users` and `groups` sections have a new field `should_exist`. If specified and false, Ignition will delete the specified user or group if it exists. + + +```yaml +variant: fcos +version: 1.2.0 +passwd: + users: + - name: core + should_exist: false + groups: + - name: core + should_exist: false +``` + +### Google Cloud Storage URL support + +The sections which allow fetching a remote URL now accept Google Cloud Storage (`gs://`) URLs in the `source` field. + + +```yaml +variant: fcos +version: 1.2.0 +storage: + files: + - path: /etc/example + mode: 0644 + contents: + source: gs://bucket/object +``` + +## From Version 1.0.0 to 1.1.0 + +There are no breaking changes between versions 1.0.0 and 1.1.0 of the `fcos` configuration specification. Any valid 1.0.0 configuration can be updated to a 1.1.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### Embedding local files in configs + +The config `merge` and `replace` sections, the `certificate_authorities` section, and the files `contents` and `append` sections gained a new field called `local`, which is mutually exclusive with the `source` and `inline` fields. It causes the contents of a file from the system running Butane to be embedded in the config. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: fcos +version: 1.1.0 +ignition: + config: + merge: + - local: config.ign + security: + tls: + certificate_authorities: + - local: ca.pem +storage: + files: + - path: /opt/file + contents: + local: file + append: + - local: file-epilogue + mode: 0644 +``` + +### Embedding directory trees in configs + +The `storage` section gained a new subsection called `trees`. It is a list of directory trees on the system running Butane whose files, directories, and symlinks will be embedded in the config. By default, the resulting filesystem objects are owned by `root:root`, directory modes are set to 0755, and file modes are set to 0755 if the source file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating an entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + +Tree paths are relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + trees: + - local: tree + path: /etc/files + files: + - path: /etc/files/overridden-file + mode: 0600 + user: + id: 500 + group: + id: 501 +``` + +### Inline contents on certificate authorities and merged configs + +The `certificate_authorities` section now supports inline contents via the `inline` field. The config `merge` and `replace` sections also now support `inline`, but using this functionality is not recommended. + + +```yaml +variant: fcos +version: 1.1.0 +ignition: + config: + merge: + - inline: | + {"ignition": {"version": "3.1.0"}} + security: + tls: + certificate_authorities: + - inline: | + -----BEGIN CERTIFICATE----- + MIICzTCCAlKgAwIBAgIJALTP0pfNBMzGMAoGCCqGSM49BAMCMIGZMQswCQYDVQQG + EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj + bzETMBEGA1UECgwKQ29yZU9TIEluYzEUMBIGA1UECwwLRW5naW5lZXJpbmcxEzAR + BgNVBAMMCmNvcmVvcy5jb20xHTAbBgkqhkiG9w0BCQEWDm9lbUBjb3Jlb3MuY29t + MB4XDTE4MDEyNTAwMDczOVoXDTI4MDEyMzAwMDczOVowgZkxCzAJBgNVBAYTAlVT + MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRMw + EQYDVQQKDApDb3JlT1MgSW5jMRQwEgYDVQQLDAtFbmdpbmVlcmluZzETMBEGA1UE + AwwKY29yZW9zLmNvbTEdMBsGCSqGSIb3DQEJARYOb2VtQGNvcmVvcy5jb20wdjAQ + BgcqhkjOPQIBBgUrgQQAIgNiAAQDEhfHEulYKlANw9eR5l455gwzAIQuraa049Rh + vM7PPywaiD8DobteQmE8wn7cJSzOYw6GLvrL4Q1BO5EFUXknkW50t8lfnUeHveCN + sqvm82F1NVevVoExAUhDYmMREa6jZDBiMA8GA1UdEQQIMAaHBH8AAAEwHQYDVR0O + BBYEFEbFy0SPiF1YXt+9T3Jig2rNmBtpMB8GA1UdIwQYMBaAFEbFy0SPiF1YXt+9 + T3Jig2rNmBtpMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDaQAwZgIxAOul + t3MhI02IONjTDusl2YuCxMgpy2uy0MPkEGUHnUOsxmPSG0gEBCNHyeKVeTaPUwIx + AKbyaAqbChEy9CvDgyv6qxTYU+eeBImLKS3PH2uW5etc/69V/sDojqpH3hEffsOt + 9g== + -----END CERTIFICATE----- +``` + +### Compression support for certificate authorities and merged configs + +The config `merge` and `replace` sections and the `certificate_authorities` section now support gzip-compressed resources via the `compression` field. `gzip` compression is supported for all URL schemes except `s3`. + + +```yaml +variant: fcos +version: 1.1.0 +ignition: + config: + merge: + - source: https://secure.example.com/example.ign.gz + compression: gzip + security: + tls: + certificate_authorities: + - source: https://example.com/ca.pem.gz + compression: gzip +``` + +### SHA-256 resource verification + +All `verification.hash` fields now support the `sha256` hash type. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + files: + - path: /etc/hosts + mode: 0644 + contents: + source: https://example.com/etc/hosts + verification: + hash: sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +``` + +### Automatic generation of mount units + +The `filesystems` section gained a new `with_mount_unit` field. If `true`, a generic mount unit will be automatically generated for the specified filesystem. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + filesystems: + - path: /var/data + device: /dev/vdb1 + format: ext4 + with_mount_unit: true +``` + +### Filesystem mount options + +The `filesystems` section gained a new `mount_options` field. It is a list of options Ignition should pass to `mount -o` when mounting the specified filesystem. This is useful for mounting btrfs subvolumes. If the `with_mount_unit` field is `true`, this field also affects mount options used by the provisioned system when mounting the filesystem. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + filesystems: + - path: /var/data + device: /dev/vdb1 + wipe_filesystem: false + format: btrfs + mount_options: + - subvolid=5 + with_mount_unit: true +``` + +### Custom HTTP headers + +The sections which allow fetching a remote URL — config `merge` and `replace`, `certificate_authorities`, and file `contents` and `append` — gained a new field called `http_headers`. This field can be set to an array of HTTP headers which will be added to an HTTP or HTTPS request. Custom headers can override Ignition's default headers, and will not be retained across HTTP redirects. + +During config merging, if a child config specifies a header `name` but not a corresponding `value`, any header with that `name` in the parent config will be removed. + + +```yaml +variant: fcos +version: 1.1.0 +storage: + files: + - path: /etc/hosts + mode: 0644 + contents: + source: https://example.com/etc/hosts + http_headers: + - name: Authorization + value: Basic YWxhZGRpbjpvcGVuc2VzYW1l + - name: User-Agent + value: Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1) +``` + +### HTTP proxies + +The `ignition` section gained a new field called `proxy`. It allows configuring proxies for HTTP and HTTPS requests, as well as exempting certain hosts from proxying. + +The `https_proxy` field specifies the proxy URL for HTTPS requests. The `http_proxy` field specifies the proxy URL for HTTP requests, and also for HTTPS requests if `https_proxy` is not specified. The `no_proxy` field lists specifiers of hosts that should not be proxied, in any of several formats: + +- An IP address prefix (`1.2.3.4`) +- An IP address prefix in CIDR notation (`1.2.3.4/8`) +- A domain name, matching the domain and its subdomains (`example.com`) +- A domain name, matching subdomains only (`.example.com`) +- A wildcard matching all hosts (`*`) + +IP addresses and domain names can also include a port number (`1.2.3.4:80`). + + +```yaml +variant: fcos +version: 1.1.0 +ignition: + proxy: + http_proxy: https://proxy.example.net/ + https_proxy: https://secure.proxy.example.net/ + no_proxy: + - www.example.net +storage: + files: + - path: /etc/hosts + mode: 0644 + contents: + source: https://example.com/etc/hosts +``` diff --git a/butane/docs/upgrading-flatcar.md b/butane/docs/upgrading-flatcar.md new file mode 100644 index 000000000..4d5518fbf --- /dev/null +++ b/butane/docs/upgrading-flatcar.md @@ -0,0 +1,95 @@ +--- +title: Flatcar +parent: Upgrading configs +nav_order: 2 +--- + +# Upgrading Flatcar configs + +Occasionally, changes are made to Flatcar Butane configs (those that specify `variant: flatcar`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} + +## From Version 1.0.0 to Version 1.1.0 + +There are no breaking changes between versions 1.0.0 and 1.1.0 of the `flatcar` configuration specification. Any valid 1.0.0 configuration can be updated to a 1.1.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### LUKS discard + +The `luks` section gained a new `discard` field. If specified and true, the LUKS volume will issue discard commands to the underlying block device when blocks are freed. This improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. + + +```yaml +variant: flatcar +version: 1.1.0 +storage: + luks: + - name: luks-static + device: /dev/sdb + discard: true + key_file: + inline: REPLACE-THIS-WITH-YOUR-KEY-MATERIAL +``` + +### LUKS open options + +The `luks` section gained a new `open_options` field. It is a list of options Ignition should pass to `cryptsetup luksOpen` when unlocking the volume. Ignition also passes `--persistent`, so any options that support persistence will be saved to the volume and automatically used for future unlocks. Any options that do not support persistence will only be applied to Ignition's initial unlock of the volume. + + +```yaml +variant: flatcar +version: 1.1.0 +storage: + luks: + - name: luks-static + device: /dev/sdb + open_options: + - "--perf-no_read_workqueue" + - "--perf-no_write_workqueue" + key_file: + inline: REPLACE-THIS-WITH-YOUR-KEY-MATERIAL +``` + +### AWS S3 access point ARN support + +The sections which allow fetching a remote URL now accept AWS S3 access point ARNs (`arn:aws:s3:::accesspoint//object/`) in the `source` field. + + +```yaml +variant: flatcar +version: 1.1.0 +storage: + files: + - path: /etc/example + mode: 0644 + contents: + source: arn:aws:s3:us-west-1:123456789012:accesspoint/test/object/some/path +``` + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: flatcar +version: 1.1.0 +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` diff --git a/butane/docs/upgrading-openshift.md b/butane/docs/upgrading-openshift.md new file mode 100644 index 000000000..1e1efe49e --- /dev/null +++ b/butane/docs/upgrading-openshift.md @@ -0,0 +1,438 @@ +--- +title: OpenShift +parent: Upgrading configs +nav_order: 3 +--- + +# Upgrading OpenShift configs + +Occasionally, changes are made to OpenShift Butane configs (those that specify `variant: openshift`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} + +## From Version 4.21.0 to 4.22.0 + +There are no breaking changes between versions 4.21.0 and 4.22.0 of the `openshift` configuration specification. Any valid 4.21.0 configuration can be updated to a 4.22.0 configuration by changing the version string in the config. + +## From Version 4.20.0 to 4.21.0 + +There are no breaking changes between versions 4.20.0 and 4.21.0 of the `openshift` configuration specification. Any valid 4.20.0 configuration can be updated to a 4.21.0 configuration by changing the version string in the config. + +## From Version 4.19.0 to 4.20.0 + +There are no breaking changes between versions 4.19.0 and 4.20.0 of the `openshift` configuration specification. Any valid 4.19.0 configuration can be updated to a 4.20.0 configuration by changing the version string in the config. + +## From Version 4.18.0 to 4.19.0 + +There are no breaking changes between versions 4.18.0 and 4.19.0 of the `openshift` configuration specification. Any valid 4.18.0 configuration can be updated to a 4.19.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### LUKS CEX support + +The `luks` sections in `storage` and `boot_device` gained a `cex` field. If enabled, this will configure an encrypted root filesystem on a s390x system using IBM Crypto Express (CEX) card. + + +```yaml +variant: openshift +version: 4.19.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +openshift: + kernel_arguments: + - rd.luks.key=/etc/luks/cex.key +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + cex: + enabled: true +``` + +### Boot_Device Layouts s390x support + +The `boot_device` section gained support for the following layouts `s390x-eckd`, `s390x-zfcp`, `s390x-virt`. This enables the use of the `boot_device` sugar for s390x systems. + +The `s390x-eckd` layout enables configuration of an encrypted root filesystem for a DASD device. + + +```yaml +variant: openshift +version: 4.19.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +boot_device: + layout: s390x-eckd + luks: + device: /dev/dasda + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +The `s390x-zfcp` layout enables configuration of an encrypted root filesystem for a zFCP device. + + +```yaml +variant: openshift +version: 4.19.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +boot_device: + layout: s390x-zfcp + luks: + device: /dev/sdb + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +The `s390x-virt` layout enables configuration of an encrypted root filesystem for KVM. + + +```yaml +variant: openshift +version: 4.19.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +boot_device: + layout: s390x-virt + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT +``` + +## From Version 4.17.0 to 4.18.0 + +There are no breaking changes between versions 4.17.0 and 4.18.0 of the `openshift` configuration specification. Any valid 4.17.0 configuration can be updated to a 4.18.0 configuration by changing the version string in the config. + +## From Version 4.16.0 to 4.17.0 + +There are no breaking changes between versions 4.16.0 and 4.17.0 of the `openshift` configuration specification. Any valid 4.16.0 configuration can be updated to a 4.17.0 configuration by changing the version string in the config. + +## From Version 4.15.0 to 4.16.0 + +There are no breaking changes between versions 4.15.0 and 4.16.0 of the `openshift` configuration specification. Any valid 4.15.0 configuration can be updated to a 4.16.0 configuration by changing the version string in the config. + +## From Version 4.14.0 to 4.15.0 + +There are no breaking changes between versions 4.14.0 and 4.15.0 of the `openshift` configuration specification. Any valid 4.14.0 configuration can be updated to a 4.15.0 configuration by changing the version string in the config. + +## From Version 4.13.0 to 4.14.0 + +There are no breaking changes between versions 4.13.0 and 4.14.0 of the `openshift` configuration specification. Any valid 4.13.0 configuration can be updated to a 4.14.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.service +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +### Offline Tang provisioning + +The `tang` sections in `storage.luks` and `boot_device.luks` gained a new `advertisement` field. If specified, Ignition will use it to provision the Tang server binding rather than fetching the advertisement from the server at runtime. This allows the server to be unavailable at provisioning time. The advertisement can be obtained from the server with `curl http://tang.example.com/adv`. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +boot_device: + luks: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + advertisement: "{\"payload\": \"...\", \"protected\": \"...\", \"signature\": \"...\"}" +storage: + luks: + - name: luks-tang + device: /dev/sdb + clevis: + tang: + - url: https://tang.example.com + thumbprint: REPLACE-THIS-WITH-YOUR-TANG-THUMBPRINT + advertisement: "{\"payload\": \"...\", \"protected\": \"...\", \"signature\": \"...\"}" +``` + +### LUKS discard + +The `luks` sections in `storage` and `boot_device` gained a new `discard` field. If specified and true, the LUKS volume will issue discard commands to the underlying block device when blocks are freed. This improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +boot_device: + luks: + discard: true + tpm2: true +storage: + luks: + - name: luks-tpm + device: /dev/sdb + discard: true + clevis: + tpm2: true +``` + +### LUKS open options + +The `storage.luks` section gained a new `open_options` field. It is a list of options Ignition should pass to `cryptsetup luksOpen` when unlocking the volume. Ignition also passes `--persistent`, so any options that support persistence will be saved to the volume and automatically used for future unlocks. Any options that do not support persistence will only be applied to Ignition's initial unlock of the volume. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +storage: + luks: + - name: luks-tpm + device: /dev/sdb + open_options: + - "--perf-no_read_workqueue" + - "--perf-no_write_workqueue" + clevis: + tpm2: true +``` + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` + +### Automatic generation of systemd swap units + +The `with_mount_unit` field of the `filesystems` section can now be set to `true` if the `format` field is set to `swap`. Butane will generate a systemd swap unit for the specified swap area. + + +```yaml +variant: openshift +version: 4.14.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +storage: + filesystems: + - device: /dev/vdb1 + format: swap + wipe_filesystem: true + with_mount_unit: true +``` + +## From Version 4.12.0 to 4.13.0 + +There are no breaking changes between versions 4.12.0 and 4.13.0 of the `openshift` configuration specification. Any valid 4.12.0 configuration can be updated to a 4.13.0 configuration by changing the version string in the config. + +The following is a list of notable new features, deprecations, and changes. + +### User passwords + +The `passwd.users` section enabled the `password_hash` field, which sets the password hash for an account. The `users` section continues to support only the `core` user. + + +```yaml +variant: openshift +version: 4.13.0 +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: core-password +passwd: + users: + - name: core + password_hash: $y$j9T$nQ... +``` + +## From Version 4.11.0 to 4.12.0 + +There are no breaking changes between versions 4.11.0 and 4.12.0 of the `openshift` configuration specification. Any valid 4.11.0 configuration can be updated to a 4.12.0 configuration by changing the version string in the config. + +## From Version 4.10.0 to 4.11.0 + +There are no breaking changes between versions 4.10.0 and 4.11.0 of the `openshift` configuration specification. Any valid 4.10.0 configuration can be updated to a 4.11.0 configuration by changing the version string in the config. + +## From Version 4.9.0 to 4.10.0 + +There are no breaking changes between versions 4.9.0 and 4.10.0 of the `openshift` configuration specification. Any valid 4.9.0 configuration can be updated to a 4.10.0 configuration by changing the version string in the config. + +### Resource compression + +Resource compression, which was disabled in all `openshift` specs in Butane 0.12.1, is re-introduced in this spec version. The `compression` field can be set to `gzip` to decompress gzip-compressed resources. In addition, Butane may automatically compress resources specified with `inline` or `local`. + + +```yaml +variant: openshift +version: 4.10.0 +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: config-openshift +storage: + files: + - path: /opt/file2 + contents: + source: data:;base64,H4sIAAAAAAAC/zSQQY4bMQwE7/OKfsBgXpHccs0DGKntEJBIWSINP3+htfcmQECxq/74ZIeOlR3Vm08sDUhnnChuiyUYOSFVh66idgebxonFiuoHNVf3imAfPqFWtGpNC2SgyT+fBOONJrrcTSBNHykX8DcOmnZIRdf9eNJU+olH6oL5ipkVfHEWDQl1Q7YmvfgbreswXbpPfTN1gC9QULx3r/42eKTEBfzaTMkgdObkx1btmByT/2mVUwNqeHrLERLEc7uCaxFFW/tpRDBxy7tKHLYXYchUiZwX8PtVOIK5S1rASxEWCZQcWiUkYG4Y07XS4jzWjqWGkm3INoffblpUULk492/3tnfITqQVXJ+02a/jKwAA//+jjAk6wQEAAA== + compression: gzip + mode: 0644 +``` + +## From Version 4.8.0 to 4.9.0 + +There are no functionality changes between versions 4.8.0 and 4.9.0 of the `openshift` configuration specification. Any valid 4.8.0 configuration can be updated to a 4.9.0 configuration by changing the version string in the config. + +## From `rhcos` Version 0.1.0 to `openshift` Version 4.8.0 + +The new `openshift` config variant is intended to work both on the OpenShift Container Platform with RHEL CoreOS, and on OKD with Fedora CoreOS. The `rhcos` variant is no longer accepted by Butane. + +The `openshift` 4.8.0 specification is not backward-compatible with the `rhcos` 0.1.0 specification. It adds new mandatory metadata fields and removes certain Ignition config fields. In addition, `openshift` configs are transpiled to an OpenShift [MachineConfig] rather than an Ignition config by default. A valid `rhcos` 0.1.0 configuration can be updated to an `openshift` 4.8.0 configuration by changing the variant and version strings and then correcting any errors reported during transpilation. + +The following is a list of breaking changes and notable new features. + +### MachineConfig generation + +By default, Butane transpiles an `openshift` Butane config into an OpenShift [MachineConfig]. Butane produces an Ignition config if the `-r` or `--raw` option is specified on the Butane command line. + +### Removed config fields + +The config no longer allows certain Ignition config fields that are rejected or discouraged by the OpenShift [Machine Config Operator]. + +In the `storage` section, `directories` and `links` are removed, along with `append` in `files`. Local file trees referenced in `trees` must not contain symlinks. + +In the `passwd` section, `groups` is removed. All fields in `users` are removed except for `name` (which must be set to `core`) and `ssh_authorized_keys`. + +### MachineConfig metadata fields + +The config gained a new top-level `metadata` section containing metadata for the generated [MachineConfig]. The mandatory `name` field specifies a [name for the Kubernetes MachineConfig resource][k8s-names]. The `labels` field specifies a map of key-value pairs to be applied to the MachineConfig resource as [Kubernetes labels][k8s-labels]. The `machineconfiguration.openshift.io/role` label is required. + +The `metadata` section is ignored when generating a raw Ignition config using the `-r` or `--raw` option. + + +```yaml +variant: openshift +version: 4.8.0 +metadata: + name: minimal-config + labels: + machineconfiguration.openshift.io/role: worker +``` + +### MCO settings + +The config gained a new top-level `openshift` section specifying [configuration][MCO settings] for the [Machine Config Operator]. The `extensions` field lists [RHCOS extension modules] to be installed on the node. The `fips` field enables [FIPS mode] when set to `true`. The `kernel_arguments` field specifies a list of [arguments][kernel arguments] to be added to the kernel command line. The `kernel_type` field can be set to `realtime` to use the [real-time kernel] on the node. + +Fields in the `openshift` section are not included in a raw Ignition config generated using the `-r` or `--raw` option. + + +```yaml +variant: openshift +version: 4.8.0 +metadata: + name: config-openshift + labels: + machineconfiguration.openshift.io/role: worker +openshift: + extensions: + - usbguard + fips: true + kernel_arguments: + - console=ttyS1,115200 + kernel_type: realtime +``` + +### FIPS configuration for LUKS + +When the `fips` field in the `openshift` section is set to `true`, LUKS volumes specified in the config (but not in any referenced configs) are configured to use a cipher compatible with [FIPS 140-2]. This cipher is applied to LUKS volumes specified in the `luks` subsections of the `storage` and `boot_device` sections. + + +```yaml +variant: openshift +version: 4.8.0 +metadata: + name: fips-luks + labels: + machineconfiguration.openshift.io/role: worker +openshift: + fips: true +boot_device: + luks: + tpm2: true +``` + +[FIPS 140-2]: https://csrc.nist.gov/publications/detail/fips/140/2/final +[FIPS mode]: https://docs.openshift.com/container-platform/4.7/installing/installing-fips.html +[k8s-names]: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +[k8s-labels]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +[kernel arguments]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#nodes-nodes-kernel-arguments_post-install-machine-configuration-tasks +[Machine Config Operator]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#understanding-the-machine-config-operator +[MachineConfig]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#machine-config-overviewpost-install-machine-configuration-tasks +[MCO settings]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#what-can-you-change-with-machine-configs +[real-time kernel]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#nodes-nodes-rtkernel-arguments_post-install-machine-configuration-tasks +[RHCOS extension modules]: https://docs.openshift.com/container-platform/4.7/post_installation_configuration/machine-configuration-tasks.html#rhcos-add-extensions_post-install-machine-configuration-tasks diff --git a/butane/docs/upgrading-r4e.md b/butane/docs/upgrading-r4e.md new file mode 100644 index 000000000..bd1cdfdda --- /dev/null +++ b/butane/docs/upgrading-r4e.md @@ -0,0 +1,59 @@ +--- +title: RHEL for Edge +parent: Upgrading configs +nav_order: 4 +--- + +# Upgrading RHEL for Edge configs + +Occasionally, changes are made to RHEL for Edge Butane configs (those that specify `variant: r4e`) that break backward compatibility. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. This document serves to detail each of the breaking changes and tries to provide some reasoning for the change. This does not cover all of the changes to the spec - just those that need to be considered when migrating from one version to the next. + +{: .no_toc } + +1. TOC +{:toc} + +## From Version 1.0.0 to Version 1.1.0 + +There are no breaking changes between versions 1.0.0 and 1.1.0 of the `r4e` configuration specification. Any valid 1.0.0 configuration can be updated to a 1.1.0 configuration by changing the version string in the config. + +The following is a list of notable new features. + +### AWS S3 access point ARN support + +The sections which allow fetching a remote URL now accept AWS S3 access point ARNs (`arn:aws:s3:::accesspoint//object/`) in the `source` field. + + +```yaml +variant: r4e +version: 1.1.0 +storage: + files: + - path: /etc/example + mode: 0644 + contents: + source: arn:aws:s3:us-west-1:123456789012:accesspoint/test/object/some/path +``` + +### Local SSH key and systemd unit references + +SSH keys and systemd units are now embeddable via file references to local files. The specified path is relative to a local _files-dir_, specified with the `-d`/`--files-dir` option to Butane. If no _files-dir_ is specified, this functionality is unavailable. + + +```yaml +variant: r4e +version: 1.1.0 +systemd: + units: + - name: example.service + contents_local: example.service + - name: example-drop-in.service + dropins: + - name: example-drop-in.conf + contents_local: example.conf +passwd: + users: + - name: core + ssh_authorized_keys_local: + - id_rsa.pub +``` diff --git a/butane/docs/upgrading.md b/butane/docs/upgrading.md new file mode 100644 index 000000000..a364c49b8 --- /dev/null +++ b/butane/docs/upgrading.md @@ -0,0 +1,16 @@ +--- +has_children: true +nav_order: 5 +has_toc: false +--- + +# Upgrading configs + +Occasionally, changes are made to Butane configuration specifications that break backward compatibility or add new functionality. While this is not a concern for running machines, since Ignition only runs one time during first boot, it is a concern for those who maintain configuration files. + +For details about changes in new versions of Butane config specs, see the guide for your specific config variant: + +- [Fedora CoreOS](upgrading-fcos.md) (`fcos`) +- [Flatcar](upgrading-flatcar.md) (`flatcar`) +- [OpenShift](upgrading-openshift.md) (`openshift`) +- [RHEL for Edge](upgrading-r4e.md) (`r4e`) diff --git a/butane/generate b/butane/generate new file mode 100755 index 000000000..e5619a8dc --- /dev/null +++ b/butane/generate @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Generating docs..." +eval $(go env) +if [ -z ${BIN_PATH+a} ]; then + BIN_PATH=${PWD}/bin/$(go env GOARCH) +fi +go build -o ${BIN_PATH}/doc internal/doc/main.go +${BIN_PATH}/doc ${PWD}/docs diff --git a/butane/go.mod b/butane/go.mod new file mode 100644 index 000000000..759a02f08 --- /dev/null +++ b/butane/go.mod @@ -0,0 +1,28 @@ +module github.com/coreos/butane + +go 1.24.0 + +toolchain go1.24.5 + +require ( + github.com/clarketm/json v1.17.1 + github.com/coreos/go-semver v0.3.1 + github.com/coreos/go-systemd/v22 v22.7.0 + github.com/coreos/ignition/v2 v2.26.0 + github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/vincent-petithory/dataurl v1.0.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect + github.com/coreos/go-json v0.0.0-20230131223807-18775e0fb4fb // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) diff --git a/butane/go.sum b/butane/go.sum new file mode 100644 index 000000000..a17489874 --- /dev/null +++ b/butane/go.sum @@ -0,0 +1,45 @@ +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/clarketm/json v1.17.1 h1:U1IxjqJkJ7bRK4L6dyphmoO840P6bdhPdbbLySourqI= +github.com/clarketm/json v1.17.1/go.mod h1:ynr2LRfb0fQU34l07csRNBTcivjySLLiY1YzQqKVfdo= +github.com/coreos/go-json v0.0.0-20230131223807-18775e0fb4fb h1:rmqyI19j3Z/74bIRhuC59RB442rXUazKNueVpfJPxg4= +github.com/coreos/go-json v0.0.0-20230131223807-18775e0fb4fb/go.mod h1:rcFZM3uxVvdyNmsAV2jopgPD1cs5SPWJWU5dOz2LUnw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/coreos/ignition/v2 v2.25.1 h1:mrXOVwb4ZPlLG1tIki5em604Yiz5oRFlWkISrEYpHds= +github.com/coreos/ignition/v2 v2.25.1/go.mod h1:Px9MZK4oLhMUM3QMzzRhKbHowc5Hkf+VUu67i0gmsNw= +github.com/coreos/ignition/v2 v2.26.0 h1:Db4IO5ydZOMnJYINgbPuXlwAH1ul0/V71h20iKyBi6k= +github.com/coreos/ignition/v2 v2.26.0/go.mod h1:2f48nEXqnh1BU69Yq3lIWDvaYFSFQVZoFB7rB7wz7DY= +github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 h1:uSmlDgJGbUB0bwQBcZomBTottKwEDF5fF8UjSwKSzWM= +github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687/go.mod h1:Salmysdw7DAVuobBW/LwsKKgpyCPHUhjyJoMJD+ZJiI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= +github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/butane/internal/doc/butane.yaml b/butane/internal/doc/butane.yaml new file mode 100644 index 000000000..81f6341b7 --- /dev/null +++ b/butane/internal/doc/butane.yaml @@ -0,0 +1,471 @@ +resource: + children: + - name: source + # In Butane, source is never required because inline and local are + # possible alternatives. The exception is fcos 1.0.0, which doesn't + # have inline or local. + required-if: + - variant: fcos + max: 1.0.0 + transforms: + - regex: $ + replacement: " Mutually exclusive with `inline` and `local`." + - regex: " and `local`" + replacement: "" + if: + - variant: fcos + max: 1.0.0 + - name: inline + after: source + desc: "the contents of the %TYPE%. Mutually exclusive with `source` and `local`." + transforms: + - regex: " and `local`" + replacement: "" + if: + - variant: fcos + max: 1.0.0 + - name: local + after: source + desc: "a local path to the contents of the %TYPE%, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `source` and `inline`." + +mode: + # File mode transforms. + transforms: + # YAML allows writing octal modes directly + - regex: 'Note that the mode must be .+\)\. ' + replacement: "" + # New Ignition spec supports special bits but MCO doesn't + - regex: are supported + replacement: are not supported + if: + - variant: openshift + min: 4.22.0 + +append-contents-local: + # Mention contents_local on specs that support it. + transforms: + - regex: $ + replacement: " Mutually exclusive with `contents_local`." + # and remove it again for old specs + - regex: " Mutually exclusive with `contents_local`." + replacement: "" + if: + - variant: fcos + max: 1.4.0 + - variant: flatcar + max: 1.0.0 + - variant: openshift + max: 4.13.0 + - variant: r4e + max: 1.0.0 + +root: + children: + - name: variant + after: ^ + desc: "used to differentiate configs for different operating systems. Must be `%VARIANT%` for this specification." + transforms: + - regex: "%VARIANT%" + replacement: fcos + if: + - variant: fcos + - regex: "%VARIANT%" + replacement: flatcar + if: + - variant: flatcar + - regex: "%VARIANT%" + replacement: openshift + if: + - variant: openshift + - regex: "%VARIANT%" + replacement: r4e + if: + - variant: r4e + - name: version + after: ^ + desc: "the semantic version of the spec for this document. This document is for version `%VERSION%` and generates Ignition configs with version `%ignition_version%`." + transforms: + - regex: "%VERSION%" + replacement: "%fcos_version%" + if: + - variant: fcos + - regex: "%VERSION%" + replacement: "%flatcar_version%" + if: + - variant: flatcar + - regex: "%VERSION%" + replacement: "%openshift_version%" + if: + - variant: openshift + - regex: "%VERSION%" + replacement: "%r4e_version%" + if: + - variant: r4e + - name: metadata + after: ^ + desc: metadata about the generated MachineConfig resource. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + required: true + children: + - name: name + desc: a unique [name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names) for this MachineConfig resource. + - name: labels + desc: string key/value pairs to apply as [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to this MachineConfig resource. `machineconfiguration.openshift.io/role` is required. + required: true + - name: ignition + # Ignition configs require ignition because they require ignition.version. + # Butane configs don't have ignition.version. + required: false + children: + - name: config + children: + - name: merge + children: + - name: source + # first merge with the Ignition root needs to add the + # field; the merge with the component happens afterward + after: $ + transforms: + # no inline directive in fcos 1.0.0 + - regex: " Mutually exclusive with `inline`." + replacement: "" + if: + - variant: fcos + max: 1.0.0 + - name: replace + children: + - name: source + # first merge with the Ignition root needs to add the + # field; the merge with the component happens afterward + after: $ + transforms: + # no inline directive in fcos 1.0.0 + - regex: " Mutually exclusive with `inline`." + replacement: "" + if: + - variant: fcos + max: 1.0.0 + - name: security + children: + - name: tls + children: + - name: certificate_authorities + transforms: + - regex: "unique `source`" + replacement: "$0, `inline`, or `local`" + # and then undo it for fcos 1.0.0 + - regex: ", `inline`, or `local`" + replacement: "" + if: + - variant: fcos + max: 1.0.0 + children: + - name: source + transforms: + # no inline directive in fcos 1.0.0 + - regex: " Mutually exclusive with `inline`." + replacement: "" + if: + - variant: fcos + max: 1.0.0 + - name: inline + # first merge with the Ignition root needs to add the + # field; the merge with the component happens afterward + after: $ + transforms: + - regex: "%TYPE%" + replacement: "certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates" + - name: local + # first merge with the Ignition root needs to add the + # field; the merge with the component happens afterward + after: $ + transforms: + - regex: '%TYPE%(, relative to the[^.]+)\.' + replacement: "certificate bundle (in PEM format)$1. The bundle can contain multiple concatenated certificates." + - name: proxy + transforms: + # Snake-case cross-references between children + - regex: "`(https|no)Proxy`" + replacement: "`${1}_proxy`" + descendants: true + - name: storage + children: + - name: disks + children: + - name: device + transforms: + - regex: $ + replacement: " The boot disk can be referenced as `/dev/disk/by-id/coreos-boot-disk`." + if: + - variant: fcos + - variant: openshift + min: 4.11.0 + - name: filesystems + children: + - name: format + transforms: + - regex: "btrfs, " + replacement: "" + if: + - variant: openshift + # "none" unsupported by MCO + - regex: "swap, or none" + replacement: "or swap" + if: + - variant: openshift + min: 4.14.0 + - name: with_mount_unit + after: $ + desc: whether to additionally generate a generic mount unit for this filesystem or a swap unit for this swap area. If a more specific unit is needed, a custom one can be specified in the `systemd.units` section. The unit will be named with the [escaped](https://www.freedesktop.org/software/systemd/man/systemd-escape.html) version of the `path` or `device`, depending on the unit type. If your filesystem is located on a Tang-backed LUKS device, the unit will automatically require network access if you specify the device as `/dev/mapper/` or `/dev/disk/by-id/dm-name-`. + transforms: + # no LUKS support + - regex: ' If your filesystem is located on a Tang-backed [^.]+\.' + replacement: "" + if: + - variant: fcos + max: 1.1.0 + # no swap support + - regex: " or a swap unit for this swap area" + replacement: "" + if: + - variant: fcos + max: 1.3.0 + - variant: openshift + max: 4.13.0 + - regex: " or `device`, depending on the unit type" + replacement: "" + if: + - variant: fcos + max: 1.3.0 + - variant: openshift + max: 4.13.0 + - name: files + children: + - name: contents + children: + - name: source + transforms: + - regex: "Supported schemes are .* haven't been modified." + replacement: 'Only the [`data`](https://tools.ietf.org/html/rfc2397) scheme is supported.' + if: + - variant: openshift + - name: mode + use: mode + - name: directories + children: + - name: mode + use: mode + - name: trees + after: $ + desc: a list of local directory trees to be embedded in the config. Ownership, file modes (using `file_mode`) and directories modes (using `dir_mode`) can be specified for the tree. If not specified, ownership is not preserved and file modes are set to 0755 if the local file is executable or 0644 otherwise. Attributes of files, directories, and symlinks can be overridden by creating a corresponding entry in the `files`, `directories`, or `links` section; such `files` entries must omit `contents` and such `links` entries must omit `target`. + transforms: + - regex: Ownership, + replacement: Symlinks must not be present. $0 + if: + - variant: openshift + - regex: Attributes of files, directories, and symlinks + replacement: File attributes + if: + - variant: openshift + - regex: "`files`, `directories`, or `links`" + replacement: "`files`" + if: + - variant: openshift + - regex: "`files` (entries must omit `contents`) and such `links` entries must omit `target`." + replacement: $1. + if: + - variant: openshift + - regex: ", file modes \\(using `file_mode`\\) and directories modes \\(using `dir_mode`\\) can be specified for the tree. If not specified, ownership is not preserved and f" + replacement: " is not preserved. F" + if: + - variant: fcos + max: 1.6.0 + - variant: fiot + max: 1.0.0 + - variant: flatcar + max: 1.1.0 + - variant: openshift + max: 4.20.0 + - variant: r4e + max: 1.1.0 + children: + - name: local + desc: the base of the local directory tree, relative to the directory specified by the `--files-dir` command-line argument. + - name: path + desc: the path of the tree within the target system. Defaults to `/`. + - name: file_mode + desc: Custom permissions to apply to files + - name: dir_mode + desc: Custom permissions to apply to directories + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: fcos + max: 1.6.0 + - variant: fiot + max: 1.0.0 + - variant: flatcar + max: 1.1.0 + - variant: openshift + max: 4.20.0 + - variant: r4e + max: 1.1.0 + - name: user + desc: User owner of the tree + children: + - name: name + desc: username + - name: id + desc: uid + - name: group + desc: Group owner of the tree + children: + - name: name + desc: group name + - name: id + desc: gid + - name: systemd + children: + - name: units + children: + - name: contents + use: append-contents-local + - name: contents_local + after: contents + desc: a local path to the contents of the unit, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: dropins + children: + - name: contents + use: append-contents-local + - name: contents_local + after: contents + desc: a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: quadlets + after: units + desc: a list of Podman Quadlet files. + children: + - name: name + desc: the name of the quadlet file. + - name: rootful + desc: whether the quadlet runs as root. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`, which applies for all non-root users. Defaults to false. + - name: contents + desc: the contents of the quadlet file. Mutually exclusive with `contents_local`. + - name: contents_local + desc: a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: dropins + desc: drop-ins to override settings in the quadlet. + children: + - name: name + desc: the name of the drop-in file. + - name: contents + desc: the contents of the drop-in. Mutually exclusive with `contents_local`. + - name: contents_local + desc: a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: passwd + children: + - name: users + children: + - name: name + transforms: + - regex: $ + replacement: " Must be `core`." + if: + - variant: openshift + - name: ssh_authorized_keys + transforms: + # older specs can be used with newer OpenShift, so document + # the different authorized_keys paths + - regex: "as an SSH key fragment at `.ssh/authorized_keys.d/ignition`" + replacement: "to `.ssh/authorized_keys` (OpenShift < 4.13) or `.ssh/authorized_keys.d/ignition` (OpenShift ≥ 4.13)" + if: + - variant: openshift + max: 4.12.0 + - name: ssh_authorized_keys_local + after: ssh_authorized_keys + desc: "a list of local paths to SSH key files, relative to the directory specified by the `--files-dir` command-line argument, to be added as SSH key fragments at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. Each file may contain multiple SSH keys, one per line." + - name: boot_device + after: $ + desc: describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified. + children: + - name: layout + desc: the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`. + transforms: + - regex: "Supported values are (.*), and `x86_64`." + replacement: "Supported values are $1, `s390x-eckd`, `s390x-virt`, `s390x-zfcp`, and `x86_64`." + if: + - variant: fcos + min: 1.6.0 + - variant: openshift + min: 4.19.0 + - name: luks + desc: describes the clevis configuration for encrypting the root filesystem. + children: + - name: device + desc: the whole-disk device (not partitions), referenced by their absolute path. Must start with `/dev/dasd` for `s390x-eckd` layout or `/dev/sd` for `s390x-zfcp` layouts. + - name: tang + use: tang + - name: tpm2 + desc: whether or not to use a tpm2 device. + - name: threshold + desc: sets the minimum number of pieces required to decrypt the device. Default is 1. + - name: discard + desc: whether to issue discard commands to the underlying block device when blocks are freed. Enabling this improves performance and device longevity on SSDs and space utilization on thinly provisioned SAN devices, but leaks information about which disk blocks contain data. If omitted, it defaults to false. + - name: cex + desc: describes the IBM Crypto Express (CEX) card configuration for the luks device. + children: + - name: enabled + desc: whether or not to enable cex compatibility for luks. If omitted, defaults to false. + - name: mirror + desc: describes mirroring of the boot disk for fault tolerance. + children: + - name: devices + desc: the list of whole-disk devices (not partitions) to include in the disk array, referenced by their absolute path. At least two devices must be specified. + - name: grub + after: $ + desc: describes the desired GRUB bootloader configuration. + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: openshift + max: 4.22.0 + children: + - name: users + desc: the list of GRUB superusers. + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: openshift + max: 4.22.0 + children: + - name: name + desc: the user name. + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: openshift + max: 4.22.0 + - name: password_hash + desc: the PBKDF2 password hash, generated with `grub2-mkpasswd-pbkdf2`. + # required by validation + required: true + transforms: + - regex: ".*" + replacement: "Unsupported" + if: + - variant: openshift + max: 4.22.0 + - name: openshift + after: $ + desc: describes miscellaneous OpenShift configuration. Respected when rendering to a MachineConfig, ignored when rendering directly to an Ignition config. + children: + - name: kernel_type + desc: which kernel to use on the node. Must be `default` or `realtime`. + - name: kernel_arguments + desc: arguments to be added to the kernel command line. + - name: extensions + desc: RHCOS extensions to be installed on the node. + - name: fips + desc: whether or not to enable FIPS 140-2 compatibility. If omitted, defaults to false. diff --git a/butane/internal/doc/header.md b/butane/internal/doc/header.md new file mode 100644 index 000000000..2ed5cb12a --- /dev/null +++ b/butane/internal/doc/header.md @@ -0,0 +1,18 @@ +--- +# This file is automatically generated from internal/doc and Ignition's +# config/doc. Do not edit. +title: {{ .Variant }} v{{ .Version }} +parent: Configuration specifications +nav_order: {{ .NavOrder }} +--- + +# {{ .Variant }} Specification v{{ .Version }} + +{{ if .Version.PreRelease -}} +**Note: This configuration is experimental and has not been stabilized. It is subject to change without warning or announcement.** + +{{ end -}} +The {{ .Variant }} configuration is a YAML document conforming to the following specification, with **_italicized_** entries being optional: + +
+ diff --git a/butane/internal/doc/main.go b/butane/internal/doc/main.go new file mode 100644 index 000000000..11ea39593 --- /dev/null +++ b/butane/internal/doc/main.go @@ -0,0 +1,323 @@ +// 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 main + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/coreos/go-semver/semver" + "github.com/coreos/ignition/v2/config/doc" + "github.com/coreos/ignition/v2/config/util" + "gopkg.in/yaml.v3" + + "github.com/coreos/butane/config" + "github.com/coreos/butane/config/common" + buUtil "github.com/coreos/butane/config/util" + + base0_3 "github.com/coreos/butane/base/v0_3" + fcos1_0 "github.com/coreos/butane/config/fcos/v1_0" + fcos1_1 "github.com/coreos/butane/config/fcos/v1_1" + fcos1_2 "github.com/coreos/butane/config/fcos/v1_2" + fcos1_3 "github.com/coreos/butane/config/fcos/v1_3" + fcos1_4 "github.com/coreos/butane/config/fcos/v1_4" + fcos1_5 "github.com/coreos/butane/config/fcos/v1_5" + fcos1_6 "github.com/coreos/butane/config/fcos/v1_6" + fcos1_7 "github.com/coreos/butane/config/fcos/v1_7" + fcos1_8_exp "github.com/coreos/butane/config/fcos/v1_8_exp" + fiot1_0 "github.com/coreos/butane/config/fiot/v1_0" + fiot1_1_exp "github.com/coreos/butane/config/fiot/v1_1_exp" + flatcar1_0 "github.com/coreos/butane/config/flatcar/v1_0" + flatcar1_1 "github.com/coreos/butane/config/flatcar/v1_1" + flatcar1_2_exp "github.com/coreos/butane/config/flatcar/v1_2_exp" + openshift4_10 "github.com/coreos/butane/config/openshift/v4_10" + openshift4_11 "github.com/coreos/butane/config/openshift/v4_11" + openshift4_12 "github.com/coreos/butane/config/openshift/v4_12" + openshift4_13 "github.com/coreos/butane/config/openshift/v4_13" + openshift4_14 "github.com/coreos/butane/config/openshift/v4_14" + openshift4_15 "github.com/coreos/butane/config/openshift/v4_15" + openshift4_16 "github.com/coreos/butane/config/openshift/v4_16" + openshift4_17 "github.com/coreos/butane/config/openshift/v4_17" + openshift4_18 "github.com/coreos/butane/config/openshift/v4_18" + openshift4_19 "github.com/coreos/butane/config/openshift/v4_19" + openshift4_20 "github.com/coreos/butane/config/openshift/v4_20" + openshift4_21 "github.com/coreos/butane/config/openshift/v4_21" + openshift4_22 "github.com/coreos/butane/config/openshift/v4_22" + openshift4_23_exp "github.com/coreos/butane/config/openshift/v4_23_exp" + openshift4_8 "github.com/coreos/butane/config/openshift/v4_8" + openshift4_9 "github.com/coreos/butane/config/openshift/v4_9" + r4e1_0 "github.com/coreos/butane/config/r4e/v1_0" + r4e1_1 "github.com/coreos/butane/config/r4e/v1_1" + r4e1_2_exp "github.com/coreos/butane/config/r4e/v1_2_exp" +) + +var ( + //go:embed header.md + headerRaw string + header = template.Must(template.New("header").Parse(headerRaw)) + + //go:embed butane.yaml + butaneDocs []byte +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + if err := generate(os.Args[1]); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} + +type variant struct { + desc string + variant string + versions []version +} + +type version struct { + version string + config buUtil.Config +} + +func generate(dir string) error { + configs := []variant{ + // alphabetical order + { + "Fedora CoreOS", + "fcos", + []version{ + // inverse order of website navbar + {"1.8.0-experimental", fcos1_8_exp.Config{}}, + {"1.0.0", fcos1_0.Config{}}, + {"1.1.0", fcos1_1.Config{}}, + {"1.2.0", fcos1_2.Config{}}, + {"1.3.0", fcos1_3.Config{}}, + {"1.4.0", fcos1_4.Config{}}, + {"1.5.0", fcos1_5.Config{}}, + {"1.6.0", fcos1_6.Config{}}, + {"1.7.0", fcos1_7.Config{}}, + }, + }, + { + "Flatcar", + "flatcar", + []version{ + // inverse order of website navbar + {"1.2.0-experimental", flatcar1_2_exp.Config{}}, + {"1.0.0", flatcar1_0.Config{}}, + {"1.1.0", flatcar1_1.Config{}}, + }, + }, + { + "OpenShift", + "openshift", + []version{ + // inverse order of website navbar + {"4.23.0-experimental", openshift4_23_exp.Config{}}, + {"4.8.0", openshift4_8.Config{}}, + {"4.9.0", openshift4_9.Config{}}, + {"4.10.0", openshift4_10.Config{}}, + {"4.11.0", openshift4_11.Config{}}, + {"4.12.0", openshift4_12.Config{}}, + {"4.13.0", openshift4_13.Config{}}, + {"4.14.0", openshift4_14.Config{}}, + {"4.15.0", openshift4_15.Config{}}, + {"4.16.0", openshift4_16.Config{}}, + {"4.17.0", openshift4_17.Config{}}, + {"4.18.0", openshift4_18.Config{}}, + {"4.19.0", openshift4_19.Config{}}, + {"4.20.0", openshift4_20.Config{}}, + {"4.21.0", openshift4_21.Config{}}, + {"4.22.0", openshift4_22.Config{}}, + }, + }, + { + "RHEL for Edge", + "r4e", + []version{ + // inverse order of website navbar + {"1.2.0-experimental", r4e1_2_exp.Config{}}, + {"1.0.0", r4e1_0.Config{}}, + {"1.1.0", r4e1_1.Config{}}, + }, + }, + { + "Fedora IoT", + "fiot", + []version{ + // inverse order of website navbar + {"1.1.0-experimental", fiot1_1_exp.Config{}}, + {"1.0.0", fiot1_0.Config{}}, + }, + }, + } + + // parse and snakify Ignition components + comps, err := doc.IgnitionComponents() + if err != nil { + return err + } + for name, comp := range comps { + snakify(&comp) + comps[name] = comp + } + + // parse and merge Butane DocFile + butaneComps, err := doc.ParseComponents(bytes.NewBuffer(butaneDocs)) + if err != nil { + return err + } + if err := comps.Merge(butaneComps); err != nil { + return err + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + for i, variant := range configs { + for j, version := range variant.versions { + if err := generateOne(dir, comps, variant, version, 50*(i+1)-j); err != nil { + return fmt.Errorf("generating docs for %s %s: %w", variant.variant, version.version, err) + } + } + } + return nil +} + +func snakify(node *doc.DocNode) { + node.Name = buUtil.Snake(node.Name) + for i := range node.Children { + snakify(&node.Children[i]) + } +} + +func generateOne(dir string, comps doc.Components, variant variant, version version, navOrder int) error { + ver := *semver.New(version.version) + + // clean up any previous experimental spec doc, for + // use during spec stabilization + experimentalPath := filepath.Join(dir, fmt.Sprintf("config-%s-v%d_%d-exp.md", variant.variant, ver.Major, ver.Minor)) + if err := os.Remove(experimentalPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + + // open file + var path string + switch ver.PreRelease { + case "": + path = filepath.Join(dir, fmt.Sprintf("config-%s-v%d_%d.md", variant.variant, ver.Major, ver.Minor)) + case "experimental": + path = experimentalPath + default: + panic(fmt.Errorf("unexpected prerelease: %v", ver.PreRelease)) + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + // write header + args := struct { + Variant string + Version semver.Version + NavOrder int + }{ + Variant: variant.desc, + Version: ver, + NavOrder: navOrder, + } + if err := header.Execute(f, args); err != nil { + return fmt.Errorf("writing header: %w", err) + } + + // write docs + ignVer, err := getIgnitionVersion(variant.variant, ver) + if err != nil { + return err + } + vers := doc.VariantVersions{ + doc.IGNITION_VARIANT: ignVer, + variant.variant: ver, + } + ignore := func(path []string) bool { + filters := version.config.FieldFilters() + if filters == nil { + return false + } + var camelPath []string + for _, el := range path { + camelPath = append(camelPath, buUtil.Camel(el)) + } + pathStr := strings.Join(camelPath, ".") + if variant.variant == "openshift" { + pathStr = fmt.Sprintf("spec.config.%s", pathStr) + } + return filters.Lookup(pathStr) != nil + } + if err := comps.Generate(vers, version.config, ignore, f); err != nil { + return fmt.Errorf("generating: %w", err) + } + return nil +} + +func getIgnitionVersion(variant string, version semver.Version) (semver.Version, error) { + // generate an empty Butane config with this variant/version + // use a random OpenShift spec as a representative structure + bu, err := yaml.Marshal(openshift4_13.Config{ + Config: fcos1_3.Config{ + Config: base0_3.Config{ + Variant: variant, + Version: version.String(), + }, + }, + Metadata: openshift4_13.Metadata{ + Name: "name", + Labels: map[string]string{ + openshift4_13.ROLE_LABEL_KEY: "value", + }, + }, + }) + if err != nil { + return semver.Version{}, fmt.Errorf("generating skeleton Butane config: %w", err) + } + + // translate to Ignition config + ign, _, err := config.TranslateBytes(bu, common.TranslateBytesOptions{ + Raw: true, + }) + if err != nil { + return semver.Version{}, fmt.Errorf("translating skeleton Butane config: %w", err) + } + + // parse Ignition version + ver, _, err := util.GetConfigVersion(ign) + if err != nil { + return semver.Version{}, fmt.Errorf("getting Ignition config version: %w", err) + } + + return ver, nil +} diff --git a/butane/internal/main.go b/butane/internal/main.go new file mode 100644 index 000000000..1f8d3b47b --- /dev/null +++ b/butane/internal/main.go @@ -0,0 +1,157 @@ +// 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 main + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/pflag" + + "github.com/coreos/butane/config" + "github.com/coreos/butane/config/common" + breport "github.com/coreos/butane/internal/report" + "github.com/coreos/butane/internal/version" +) + +func fail(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} + +func isCharDevice(f *os.File) bool { + stat, err := f.Stat() + if err != nil { + return false + } + return stat.Mode()&os.ModeCharDevice != 0 +} + +func main() { + var ( + input string + output string + colorFlag string + check bool + strict bool + helpFlag bool + versionFlag bool + rawErrors bool + colorize bool + ) + options := common.TranslateBytesOptions{} + pflag.BoolVarP(&helpFlag, "help", "h", false, "show usage and exit") + pflag.BoolVarP(&versionFlag, "version", "V", false, "print the version and exit") + pflag.BoolVarP(&options.DebugPrintTranslations, "debug", "D", false, "log translations") + pflag.Lookup("debug").Hidden = true + pflag.BoolVarP(&check, "check", "c", false, "check config without producing output") + pflag.BoolVarP(&strict, "strict", "s", false, "fail on any warning") + pflag.BoolVarP(&options.Pretty, "pretty", "p", false, "output formatted json") + pflag.BoolVarP(&options.Raw, "raw", "r", false, "never wrap in a MachineConfig; force Ignition output") + pflag.BoolVar(&rawErrors, "raw-errors", false, "show raw errors, rather than pretty printing them") + pflag.StringVar(&colorFlag, "color", "auto", `control color output: "auto", "always", or "never"`) + pflag.Lookup("color").NoOptDefVal = "always" + pflag.StringVar(&colorFlag, "colour", "auto", `control color output: "auto", "always", or "never"`) + pflag.Lookup("colour").NoOptDefVal = "always" + pflag.Lookup("colour").Hidden = true + pflag.StringVar(&input, "input", "", "read from input file instead of stdin") + pflag.Lookup("input").Deprecated = "specify filename directly on command line" + pflag.Lookup("input").Hidden = true + pflag.StringVarP(&output, "output", "o", "", "write to output file instead of stdout") + pflag.StringVarP(&options.FilesDir, "files-dir", "d", "", "allow embedding local files from this directory") + + pflag.Usage = func() { + fmt.Fprintf(pflag.CommandLine.Output(), "Usage: %s [options] [input-file]\n", os.Args[0]) + fmt.Fprintf(pflag.CommandLine.Output(), "Options:\n") + pflag.PrintDefaults() + } + pflag.Parse() + + switch colorFlag { + case "always", "yes": + colorize = true + case "never", "no": + colorize = false + case "auto": + _, noColorSet := os.LookupEnv("NO_COLOR") + isTTY := isCharDevice(os.Stderr) + colorize = !noColorSet && isTTY + } + + args := pflag.Args() + if len(args) == 1 && input == "" { + input = args[0] + } else if len(args) > 0 { + pflag.Usage() + os.Exit(2) + } + + if helpFlag { + pflag.CommandLine.SetOutput(os.Stdout) + pflag.Usage() + os.Exit(0) + } + + if versionFlag { + fmt.Println(version.String) + os.Exit(0) + } + + infile := os.Stdin + filename := "" + if input != "" { + var err error + infile, err = os.Open(input) + if err != nil { + fail("failed to open %s: %v\n", input, err) + } + defer infile.Close() + filename = input + } + + dataIn, err := io.ReadAll(infile) + if err != nil { + fail("failed to read %s: %v\n", infile.Name(), err) + } + + dataOut, r, err := config.TranslateBytes(dataIn, options) + + errorString := breport.FormatError(r, filename, dataIn, colorize, rawErrors) + fmt.Fprintf(os.Stderr, "%s", errorString) + + if err != nil { + fail("Error translating config: %v\n", err) + } + if strict && len(r.Entries) > 0 { + fail("Config produced warnings and --strict was specified\n") + } + + if !check { + outfile := os.Stdout + if output != "" { + var err error + outfile, err = os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + fail("failed to open %s: %v\n", output, err) + } + defer outfile.Close() + } + + if _, err := outfile.Write(append(dataOut, '\n')); err != nil { + fail("Failed to write config to %s: %v\n", outfile.Name(), err) + } + } +} diff --git a/butane/internal/report/report.go b/butane/internal/report/report.go new file mode 100644 index 000000000..e9763068b --- /dev/null +++ b/butane/internal/report/report.go @@ -0,0 +1,157 @@ +// 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 report allows butane to pretty print errors, the error format is as follows: +package report + +import ( + "fmt" + "strings" + "unicode" + + "github.com/coreos/vcontext/report" +) + +const ( + red = "\033[1;31m" + yellow = "\033[1;33m" + cyan = "\033[1;36m" + blue = "\033[1;34m" + reset = "\033[0m" +) + +func FormatError(r report.Report, fileName string, source []byte, colorize, rawErrors bool) string { + if rawErrors { + return formatErrorSimple(r) + } else { + return formatErrorPretty(r, fileName, source, colorize) + } +} + +func formatErrorSimple(r report.Report) string { + return r.String() +} + +func formatErrorPretty(r report.Report, fileName string, source []byte, colorize bool) string { + lines := strings.Split(string(source), "\n") + var buf strings.Builder + for i, entry := range r.Entries { + if i > 0 { + buf.WriteString("\n") + } + buf.WriteString(formatErrorEntry(entry, fileName, lines, colorize)) + } + return buf.String() +} + +func color(text, code string, colorize bool) string { + if !colorize { + return text + } + return code + text + reset +} + +func severityColor(kind report.EntryKind) string { + switch kind { + case report.Error: + return red + case report.Info: + return cyan + case report.Warn: + return yellow + default: + return reset + } +} + +func writeUnderline(buf *strings.Builder, col, gutterWidth int, message, line string, colorize bool) { + underlineStart := col - 1 + rest := line[underlineStart:] + nextWhitespace := strings.IndexFunc(rest, unicode.IsSpace) + if nextWhitespace == -1 { + // If we didn't find whitespace then that means that we need to underline the entire string + nextWhitespace = len(rest) + } + underlineEnd := underlineStart + nextWhitespace + padding := strings.Repeat(" ", underlineStart) + underline := strings.Repeat("^", underlineEnd-underlineStart) + underline = color(underline, blue, colorize) + + fmt.Fprintf(buf, " %s | %s%s %s\n", + strings.Repeat(" ", gutterWidth), padding, underline, message) +} + +// formatErrorEntry will try to return the error as a pretty string in the following form +// +// error[$.boot_device.layout]: +// +// --> ../testing.bu:4:11 +// | +// 2 | version: 1.6.0 +// 3 | boot_device: +// 4 | layout: s390x-virt +// | ^^^^^^^^^^ mirroring not supported on layouts: s390x-eckd, s390x-zfcp, s390x-virt +// 5 | mirror: +// 6 | devices: +// | +func formatErrorEntry(entry report.Entry, filename string, lines []string, colorize bool) string { + if entry.Marker.StartP == nil { + return entry.String() + "\n" + } + + line := int(entry.Marker.StartP.Line) + col := int(entry.Marker.StartP.Column) + + // this should never happen as lines and cols are 1 indexed, but we'll add a check in case the vcontext library ever changes + if line < 1 || line > len(lines) || col < 1 || col > len(lines[line-1]) { + return entry.String() + "\n" + } + + var buf strings.Builder + kindColor := severityColor(entry.Kind) + kindStr := color(entry.Kind.String(), kindColor, colorize) + + path := "" + if entry.Context.Len() > 0 { + path = "[" + entry.Context.String() + "]" + } + fmt.Fprintf(&buf, "%s%s:\n", kindStr, path) + // Add information about the location of the error in the following form: + // + // " --> testing.bu:10:4" + arrow := color("-->", blue, colorize) + fmt.Fprintf(&buf, " %s %s:%d:%d\n", arrow, filename, line, col) + + // Number of lines to show before and after the error location + contextLines := 2 + contextLineInit := max(1, line-contextLines) + contextLineEnd := min(len(lines), line+contextLines) + + // width of the largest line number + gutterWidth := len(fmt.Sprintf("%d", contextLineEnd)) + fmt.Fprintf(&buf, " %s |\n", strings.Repeat(" ", gutterWidth)) + for lineNumber := contextLineInit; lineNumber <= contextLineEnd; lineNumber++ { + lineNum := color(fmt.Sprintf("%*d", gutterWidth, lineNumber), blue, colorize) + + fmt.Fprintf(&buf, " %s | %s\n", lineNum, lines[lineNumber-1]) + // Underline the error and write the error message + if lineNumber == line { + writeUnderline(&buf, col, gutterWidth, entry.Message, lines[lineNumber-1], colorize) + } + } + + // Empty line at the end + fmt.Fprintf(&buf, " %s |\n", strings.Repeat(" ", gutterWidth)) + return buf.String() +} diff --git a/butane/internal/version/version.go b/butane/internal/version/version.go new file mode 100644 index 000000000..dc8e80986 --- /dev/null +++ b/butane/internal/version/version.go @@ -0,0 +1,24 @@ +// 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 version + +import ( + "fmt" +) + +var ( + Raw = "was not built properly" + String = fmt.Sprintf("Butane %s", Raw) +) diff --git a/butane/signing-ticket.sh b/butane/signing-ticket.sh new file mode 100755 index 000000000..d06fb757c --- /dev/null +++ b/butane/signing-ticket.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Script for generating Fedora releng release signing tickets. Generated by +# https://github.com/coreos/repo-templates; do not edit downstream. + +set -euo pipefail + +usage() { + echo "Usage: $0 test " + echo "Usage: $0 ticket " + exit 1 +} + +make_script() { + sed -e "s/@@VERSION@@/$ver/g" -e "s/@@RELEASE@@/$rel/g" <<'EOF' +#!/bin/bash +set -eux -o pipefail + +# Use the Fedora 44 key for the detached signatures +KEYTOSIGNWITH='fedora-44' + +VR='@@VERSION@@-@@RELEASE@@.fc44' +RPMKEY='6d9f90a6' # Fedora 44 key + +do_sign() { + # Sign with sigul unless FAKESIGN=1 + if [ ${FAKESIGN:-0} != 1 ]; then + sigul sign-data -a $KEYTOSIGNWITH "$1" -o "$1.asc" + else + echo INVALID > "$1.asc" + fi +} + +# Grab the binaries out of the redistributable rpm +rpm="butane-redistributable-${VR}.noarch.rpm" +koji download-build --key $RPMKEY --rpm $rpm +rpm -Kv "$rpm" 2>&1 | grep -qi "${RPMKEY}" # Verify the output has the key in it +rpm2cpio $rpm | cpio -idv './usr/share/butane/butane-*' + +# Rename the binaries +mv usr/share/butane/butane-aarch64-apple-darwin \ + butane-aarch64-apple-darwin +mv usr/share/butane/butane-aarch64-unknown-linux-gnu-static \ + butane-aarch64-unknown-linux-gnu +mv usr/share/butane/butane-ppc64le-unknown-linux-gnu-static \ + butane-ppc64le-unknown-linux-gnu +mv usr/share/butane/butane-s390x-unknown-linux-gnu-static \ + butane-s390x-unknown-linux-gnu +mv usr/share/butane/butane-x86_64-apple-darwin \ + butane-x86_64-apple-darwin +mv usr/share/butane/butane-x86_64-pc-windows-gnu.exe \ + butane-x86_64-pc-windows-gnu.exe +mv usr/share/butane/butane-x86_64-unknown-linux-gnu-static \ + butane-x86_64-unknown-linux-gnu + +# Sign them +do_sign butane-aarch64-apple-darwin +do_sign butane-aarch64-unknown-linux-gnu +do_sign butane-ppc64le-unknown-linux-gnu +do_sign butane-s390x-unknown-linux-gnu +do_sign butane-x86_64-apple-darwin +do_sign butane-x86_64-pc-windows-gnu.exe +do_sign butane-x86_64-unknown-linux-gnu + +# Fix permissions and clean up +chmod go+r *.asc +rm $rpm; rmdir ./usr/share/butane; rmdir ./usr/share; rmdir ./usr +EOF +} + +make_ticket() { + sed "s/@@VERSION@@/$ver/g" <<'EOF' +TITLE: Create detached signatures for the butane @@VERSION@@ release + +Please create detached signatures for the binaries we will upload to GitHub for the `butane` @@VERSION@@ release. This is a manual process for now, pending the automation discussed in https://pagure.io/robosignatory/issue/53 and https://github.com/coreos/fedora-coreos-tracker/issues/335. + +The binaries themselves have been built in koji. Here is a small script to grab all of the rpms and the files out of the rpms and name them appropriately: + +``` +EOF + make_script + cat <<'EOF' +``` + +After running this you should end up with a directory with files in it like: + +``` +$ ls -1 +butane-aarch64-apple-darwin +butane-aarch64-apple-darwin.asc +butane-aarch64-unknown-linux-gnu +butane-aarch64-unknown-linux-gnu.asc +butane-ppc64le-unknown-linux-gnu +butane-ppc64le-unknown-linux-gnu.asc +butane-s390x-unknown-linux-gnu +butane-s390x-unknown-linux-gnu.asc +butane-x86_64-apple-darwin +butane-x86_64-apple-darwin.asc +butane-x86_64-pc-windows-gnu.exe +butane-x86_64-pc-windows-gnu.exe.asc +butane-x86_64-unknown-linux-gnu +butane-x86_64-unknown-linux-gnu.asc +``` +EOF +} + +[ "$#" -lt 2 ] && usage +cmd="$1" +ver="$2" +# Disallow version with preceding "v" +echo "$ver" | grep -q "v" && usage +# Require version-release +echo "$ver" | grep -q "-" || usage +rel="${ver#*-}" +ver="${ver%%-*}" + +case "$cmd" in +test) + [ "$#" != 3 ] && usage + dir="$3" + mkdir "$dir" + make_script > "$dir/script" + cd "$dir" && FAKESIGN=1 bash script + ;; +ticket) + make_ticket + ;; +*) + usage + ;; +esac diff --git a/butane/tag_release.sh b/butane/tag_release.sh new file mode 100755 index 000000000..9b7874b36 --- /dev/null +++ b/butane/tag_release.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Maintained in https://github.com/coreos/repo-templates +# Do not edit downstream. + +set -e + +[ $# == 2 ] || { echo "usage: $0 " && exit 1; } + +VER=$1 +COMMIT=$2 + +[[ "${VER}" =~ ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-.+)?$ ]] || { + echo "malformed version: \"${VER}\"" + exit 2 +} + +[[ "${COMMIT}" =~ ^[[:xdigit:]]+$ ]] || { + echo "malformed commit id: \"${COMMIT}\"" + exit 3 +} + +if [ -f Makefile ]; then + make +else + ./build +fi + +git tag --sign --message "Butane ${VER}" "${VER}" "${COMMIT}" +git verify-tag --verbose "${VER}" diff --git a/butane/test b/butane/test new file mode 100755 index 000000000..e912a20da --- /dev/null +++ b/butane/test @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +SRC=$(find . -name '*.go' -not -path "./vendor/*") + +echo "checking gofmt" +res=$(gofmt -d $SRC) +if [ -n "$res" ]; then + echo "$res" + exit 1 +fi + +echo "checking govet" +PKG_VET=$(go list ./... | grep --invert-match vendor) +# tests widely use unkeyed fields in composite literals. golangci-lint +# in CI does a more nuanced check. +go vet -composites=false $PKG_VET + +source ./build + +echo "Running tests" +go test ./... -cover + +csplit="" +head="" + +if [ "$(go env GOOS)" = linux ]; then + csplit="csplit" + head="head" +elif [ "$(go env GOOS)" = darwin ]; then + # macOS has BSD versions of csplit and head that behave differently; + # check whether brew/macports supplied GNU versions exist + if hash gcsplit &> /dev/null; then + csplit="gcsplit" + fi + if hash ghead &> /dev/null; then + head="ghead" + fi +elif [ "$(go env GOOS)" = windows ]; then + # if we find a Bash on Windows we can comparatively safely assume + # Git Bash with GNU utils is being used + csplit="csplit" + head="head" +fi + +if [ -n "${csplit}" ] && [ -n "${head}" ]; then + echo "Checking docs" + shopt -s nullglob + mkdir tmpdocs + trap 'rm -r tmpdocs' EXIT + # Create files-dir contents expected by configs + mkdir -p tmpdocs/files-dir/tree + touch tmpdocs/files-dir/{config.ign,ca.pem,example.conf,example.service,file,file-epilogue,local-file3} + echo "ssh-rsa AAAA" > tmpdocs/files-dir/id_rsa.pub + echo "ssh-ed25519 AAAA" > tmpdocs/files-dir/id_ed25519.pub + echo '{"ignition": {"version": "3.5.0"}}' > tmpdocs/files-dir/ignition.ign + + for doc in docs/*md + do + echo "Checking ${doc}" + # split each doc into a bunch of tmpfiles then run butane on them + sed -n '/^/,/^```$/ p' <"${doc}" \ + | ${csplit} - '//' '{*}' -z --prefix "tmpdocs/config_$(basename ${doc%.*})_" -q + + for i in tmpdocs/config_* + do + echo "Checking ${i}" + tail -n +3 "${i}" | ${head} -n -1 \ + | "${BIN_PATH}/${NAME}" --check --strict --files-dir tmpdocs/files-dir \ + || (cat -n "${i}" && false) + done + rm -f tmpdocs/config_* + done +else + # Avoid dealing with presence/behavior of csplit and head + echo "skipping docs check because GNU csplit and head are unavailable" +fi + +echo ok diff --git a/butane/tests/core/core.fmf b/butane/tests/core/core.fmf new file mode 100644 index 000000000..0aa82c8e7 --- /dev/null +++ b/butane/tests/core/core.fmf @@ -0,0 +1,7 @@ +summary: runs butane --version and checks return code +tag: + - smoke +test: | + set -x -e -o pipefail + source /tmp/butane_bin_dir + ${BUTANE_BIN_DIR}/butane --version diff --git a/butane/tests/tmt/plans/main.fmf b/butane/tests/tmt/plans/main.fmf new file mode 100644 index 000000000..d3ea8a178 --- /dev/null +++ b/butane/tests/tmt/plans/main.fmf @@ -0,0 +1,32 @@ +# This prepare is used to control when butane is installed using +# the distribution package or when it is built from source in the test environment +prepare: + - name: Set BUTANE_BIN_DIR when built from source + when: use_built_from_src is defined and use_built_from_src == true + how: shell + script: | + # This is a workaround script for the fact that the butane binary is not in the PATH + # when running the tests in the tmt environment when it is built from source. + # The butane binary is located in the tmt run instance directory and it needed + # to set a environment variable to point to the butane binary location. + set -x -e -o pipefail + echo "Preparing the test environment" + BUTANE_BIN_NAME="butane" + PARENT_DIR=$(dirname "${TMT_TREE}") + BUTANE_BIN_FULL_PATH=$(find "${PARENT_DIR}" -type f -name "${BUTANE_BIN_NAME}") + if [ -z "${BUTANE_BIN_FULL_PATH}" ]; then + echo "butane file not found." + exit 1 + fi + BUTANE_BIN_DIR=$(dirname "${BUTANE_BIN_FULL_PATH}") + echo "BUTANE_BIN_DIR=${BUTANE_BIN_DIR}" > /tmp/butane_bin_dir + - name: Install butane package + when: use_built_from_src is not defined or use_built_from_src == false + how: install + package: butane + - name: Set BUTANE_BIN_DIR when installed package + when: use_built_from_src is not defined or use_built_from_src == false + how: shell + script: | + set -x -e -o pipefail + echo "BUTANE_BIN_DIR=/usr/bin/butane" > /tmp/butane_bin_dir diff --git a/butane/tests/tmt/plans/smoke.fmf b/butane/tests/tmt/plans/smoke.fmf new file mode 100644 index 000000000..01c7ae31e --- /dev/null +++ b/butane/tests/tmt/plans/smoke.fmf @@ -0,0 +1,9 @@ +summary: Basic smoke test +tag: + - smoke +discover: + how: fmf + filter: "tag: smoke" +execute: + how: tmt + diff --git a/butane/translate/set.go b/butane/translate/set.go new file mode 100644 index 000000000..48bca81d7 --- /dev/null +++ b/butane/translate/set.go @@ -0,0 +1,208 @@ +// 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 translate + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/coreos/vcontext/path" +) + +// Translation represents how a path changes when translating. If something at $yaml.storage.filesystems.4 +// generates content at $json.systemd.units.3 a translation can represent that. This allows validation errors +// in Ignition structs to be tracked back to their source in the yaml. +type Translation struct { + From path.ContextPath + To path.ContextPath +} + +func (t Translation) String() string { + return fmt.Sprintf("%s → %s", t.From, t.To) +} + +// TranslationSet represents all of the translations that occurred. They're stored in a map from a string representation +// of the destination path to the translation struct. The map is purely an optimization to allow fast lookups. Ideally the +// map would just be from the destination path.ContextPath to the source path.ContextPath, but ContextPath contains a slice +// which are not comparable and thus cannot be used as keys in maps. +type TranslationSet struct { + FromTag string + ToTag string + Set map[string]Translation +} + +func NewTranslationSet(fromTag, toTag string) TranslationSet { + return TranslationSet{ + FromTag: fromTag, + ToTag: toTag, + Set: map[string]Translation{}, + } +} + +func (ts TranslationSet) String() string { + type entry struct { + sortKey string + formatted string + } + var entries []entry + for k, v := range ts.Set { + formatted := v.String() + // lookup key should always match To path; report if it doesn't + if k != v.To.String() { + formatted += fmt.Sprintf(" (key: %s)", k) + } + entries = append(entries, entry{ + sortKey: v.To.String(), + formatted: formatted, + }) + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].sortKey < entries[j].sortKey + }) + str := fmt.Sprintf("TranslationSet: %v → %v\n", ts.FromTag, ts.ToTag) + for _, entry := range entries { + str += entry.formatted + "\n" + } + return str +} + +// AddTranslation adds a translation to the set +func (ts TranslationSet) AddTranslation(from, to path.ContextPath) { + // create copies of the paths so if someone else changes from.Path the added translation does not change. + from = from.Copy() + to = to.Copy() + translation := Translation{ + From: from, + To: to, + } + toString := translation.To.String() + ts.Set[toString] = translation +} + +// AddFromCommonSource adds translations for all of the paths in to from a single common path. This is useful +// if one part of a config generates a large struct and all of the large struct should map to one path in the +// config being translated. +func (ts TranslationSet) AddFromCommonSource(common path.ContextPath, toPrefix path.ContextPath, to interface{}) { + v := reflect.ValueOf(to) + vPaths := prefixPaths(getAllPaths(v, ts.ToTag, true), toPrefix.Path...) + for _, toPath := range vPaths { + ts.AddTranslation(common, toPath) + } + ts.AddTranslation(common, toPrefix) +} + +// AddFromCommonObject adds translations for all of the paths in to. The paths being translated +// are prefixed by fromPrefix and the translated paths are prefixed by toPrefix. +// This is useful when we want to copy all the fields of an object to another with the same field names. +func (ts TranslationSet) AddFromCommonObject(fromPrefix path.ContextPath, toPrefix path.ContextPath, to interface{}) { + vTo := reflect.ValueOf(to) + vPaths := getAllPaths(vTo, ts.ToTag, true) + + for _, path := range vPaths { + ts.AddTranslation(fromPrefix.Append(path.Path...), toPrefix.Append(path.Path...)) + } + ts.AddTranslation(fromPrefix, toPrefix) +} + +// Merge adds all the entries to the set. It mutates the Set in place. +func (ts TranslationSet) Merge(from TranslationSet) { + for _, t := range from.Set { + ts.AddTranslation(t.From, t.To) + } +} + +// MergeP is like Merge, but it adds a prefix to the set being merged in. +func (ts TranslationSet) MergeP(prefix interface{}, from TranslationSet) { + ts.MergeP2(prefix, prefix, from) +} + +// MergeP2 is like Merge, but it adds distinct prefixes to each side of the +// set being merged in. +func (ts TranslationSet) MergeP2(fromPrefix interface{}, toPrefix interface{}, from TranslationSet) { + from = from.PrefixPaths(path.New(from.FromTag, fromPrefix), path.New(from.ToTag, toPrefix)) + ts.Merge(from) +} + +// Prefix returns a TranslationSet with all translation paths prefixed by prefix. +func (ts TranslationSet) Prefix(prefix interface{}) TranslationSet { + return ts.PrefixPaths(path.New(ts.FromTag, prefix), path.New(ts.ToTag, prefix)) +} + +// PrefixPaths returns a TranslationSet with from translation paths prefixed by +// fromPrefix and to translation paths prefixed by toPrefix. +func (ts TranslationSet) PrefixPaths(fromPrefix, toPrefix path.ContextPath) TranslationSet { + ret := NewTranslationSet(ts.FromTag, ts.ToTag) + for _, tr := range ts.Set { + ret.AddTranslation(fromPrefix.Append(tr.From.Path...), toPrefix.Append(tr.To.Path...)) + } + return ret +} + +// Descend returns the subtree of translations rooted at the specified To path. +func (ts TranslationSet) Descend(to path.ContextPath) TranslationSet { + ret := NewTranslationSet(ts.FromTag, ts.ToTag) +OUTER: + for _, tr := range ts.Set { + if len(tr.To.Path) < len(to.Path) { + // can't be in the requested subtree; skip + continue + } + for i, e := range to.Path { + if tr.To.Path[i] != e { + // not in the requested subtree; skip + continue OUTER + } + } + subtreePath := path.New(tr.To.Tag, tr.To.Path[len(to.Path):]...) + ret.AddTranslation(tr.From, subtreePath) + } + return ret +} + +// Map returns a new TranslationSet with To translation paths further +// translated through mappings. Translations not listed in mappings are +// copied unmodified. +func (ts TranslationSet) Map(mappings TranslationSet) TranslationSet { + if mappings.FromTag != ts.ToTag || mappings.ToTag != ts.ToTag { + panic(fmt.Sprintf("mappings have incorrect tag; %q != %q || %q != %q", mappings.FromTag, ts.ToTag, mappings.ToTag, ts.ToTag)) + } + ret := NewTranslationSet(ts.FromTag, ts.ToTag) + ret.Merge(ts) + for _, mapping := range mappings.Set { + if t, ok := ret.Set[mapping.From.String()]; ok { + delete(ret.Set, mapping.From.String()) + ret.AddTranslation(t.From, mapping.To) + } + } + return ret +} + +// DebugVerifyCoverage recursively checks whether every non-zero field in v +// has a translation. If translations are missing, it returns a multi-line +// error listing them. +func (ts TranslationSet) DebugVerifyCoverage(v interface{}) error { + var missingPaths []string + for _, pathToCheck := range getAllPaths(reflect.ValueOf(v), ts.ToTag, false) { + if _, ok := ts.Set[pathToCheck.String()]; !ok { + missingPaths = append(missingPaths, pathToCheck.String()) + } + } + if len(missingPaths) > 0 { + return fmt.Errorf("missing paths in TranslationSet:\n%v", strings.Join(missingPaths, "\n")) + } + return nil +} diff --git a/butane/translate/set_test.go b/butane/translate/set_test.go new file mode 100644 index 000000000..c929c6a1b --- /dev/null +++ b/butane/translate/set_test.go @@ -0,0 +1,100 @@ +// 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 translate + +import ( + "testing" + + "github.com/coreos/vcontext/path" + "github.com/stretchr/testify/assert" +) + +// mkTrans makes a TranslationSet with no tag in the paths consuming pairs of args. i.e: +// mkTrans(from1, to1, from2, to2) -> a set wiht from1->to1, from2->to2 +// This is just a shorthand for making writing tests easier +func mkTrans(paths ...path.ContextPath) TranslationSet { + ret := TranslationSet{Set: map[string]Translation{}} + if len(paths)%2 == 1 { + panic("Odd number of args to mkTrans") + } + for i := 0; i < len(paths); i += 2 { + ret.AddTranslation(paths[i], paths[i+1]) + } + return ret +} + +// fp means "fastpath"; super shorthand, we'll use it a lot +func fp(parts ...interface{}) path.ContextPath { + return path.New("", parts...) +} + +func TestTranslationSetMap(t *testing.T) { + create := func() TranslationSet { + return mkTrans( + fp(), fp(), + fp("a"), fp("A"), + fp("a", 0), fp("A", 0), + fp("a", 0, "b"), fp("A", 0, "B"), + fp("a", 0, "b", "c"), fp("A", 0, "B"), + fp("a", 0, "b", "d"), fp("A", 0, "B", 0), + fp("a", 0, "b", "e"), fp("A", 0, "B", 0, "C"), + fp("a", 0, "b", "f"), fp("A", 0, "B", 0, "D"), + fp("clobbered"), fp("A", 0, "B", 0, "G"), + fp("a", 0, "b", "g"), fp("A", 0, "B", 1), + fp("a", 0, "b", "h"), fp("A", 0, "B", 1, "E"), + fp("a", 0, "b", "i"), fp("A", 0, "B", 1, "F"), + ) + } + ts := create() + result := ts.Map(mkTrans( + fp("A", 0, "B", 0, "C"), fp("A", 0, "B", 0, "G"), + fp("A", 0, "B", 0, "D"), fp("A", 0, "H"), + fp("missing"), fp("B"), + )) + assert.Equal(t, create(), ts, "original was changed") + assert.Equal(t, mkTrans( + fp(), fp(), + fp("a"), fp("A"), + fp("a", 0), fp("A", 0), + fp("a", 0, "b"), fp("A", 0, "B"), + fp("a", 0, "b", "c"), fp("A", 0, "B"), + fp("a", 0, "b", "d"), fp("A", 0, "B", 0), + fp("a", 0, "b", "e"), fp("A", 0, "B", 0, "G"), + fp("a", 0, "b", "f"), fp("A", 0, "H"), + fp("a", 0, "b", "g"), fp("A", 0, "B", 1), + fp("a", 0, "b", "h"), fp("A", 0, "B", 1, "E"), + fp("a", 0, "b", "i"), fp("A", 0, "B", 1, "F"), + ), result, "bad mapping") +} + +func TestTranslationSetAddFromCommonSource(t *testing.T) { + type Sub struct { + C int `json:"c"` + } + type Main struct { + A string `json:"a"` + B Sub `json:"b"` + } + + expected := NewTranslationSet("yaml", "json") + expected.AddTranslation(path.New("yaml", "y"), path.New("json", "z", 0)) + expected.AddTranslation(path.New("yaml", "y", "a"), path.New("json", "z", 0, "a")) + expected.AddTranslation(path.New("yaml", "y", "b"), path.New("json", "z", 0, "b")) + expected.AddTranslation(path.New("yaml", "y", "b", "c"), path.New("json", "z", 0, "b", "c")) + + actual := NewTranslationSet("yaml", "json") + actual.AddFromCommonObject(path.New("yaml", "y"), path.New("json", "z", 0), &Main{}) + assert.Equal(t, expected, actual) +} diff --git a/butane/translate/tests/pkga/types.go b/butane/translate/tests/pkga/types.go new file mode 100644 index 000000000..46f7d8836 --- /dev/null +++ b/butane/translate/tests/pkga/types.go @@ -0,0 +1,42 @@ +// 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 pkga + +type Trivial struct { + A string + B int + C bool +} + +type Nested struct { + D string + Trivial +} + +type TrivialReordered struct { + B int + A string + C bool +} + +type HasList struct { + L []Trivial +} + +type TrivialSkip struct { + A string `butane:"auto_skip"` + B int + C bool +} diff --git a/butane/translate/tests/pkgb/types.go b/butane/translate/tests/pkgb/types.go new file mode 100644 index 000000000..ab3edccf0 --- /dev/null +++ b/butane/translate/tests/pkgb/types.go @@ -0,0 +1,42 @@ +// 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 pkgb + +type Trivial struct { + A string + B int + C bool +} + +type Nested struct { + D string + Trivial +} + +// note: struct ordering is different from pkga +type TrivialReordered struct { + A string + B int + C bool +} + +type HasList struct { + L []Nested +} + +type TrivialSkip struct { + B int + C bool +} diff --git a/butane/translate/tests/readme.txt b/butane/translate/tests/readme.txt new file mode 100644 index 000000000..7520837a3 --- /dev/null +++ b/butane/translate/tests/readme.txt @@ -0,0 +1,3 @@ +Tests for this translator are in their own package since it needs to test +translating from one package to another. This pattern should not be replicated +in other places in the codebase diff --git a/butane/translate/translate.go b/butane/translate/translate.go new file mode 100644 index 000000000..8977b4740 --- /dev/null +++ b/butane/translate/translate.go @@ -0,0 +1,276 @@ +// 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 translate + +import ( + "fmt" + "reflect" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +/* + * This is an automatic translator that replace boilerplate code to copy one + * struct into a nearly identical struct in another package. To use it first + * call NewTranslator() to get a translator instance. This can then have + * additional translation rules (in the form of functions) to translate from + * types in one struct to the other. Those functions are in the form: + * func(fromType, optionsType) -> (toType, TranslationSet, report.Report) + * These can be closures that reference the translator as well. This allows for + * manually translating some fields but resuming automatic translation on the + * other fields through the Translator.Translate() function. + */ + +const ( + TAG_KEY = "butane" + TAG_AUTO_SKIP = "auto_skip" +) + +var ( + translationsType = reflect.TypeOf(TranslationSet{}) + reportType = reflect.TypeOf(report.Report{}) +) + +// Returns if this type can be translated without a custom translator. Children or other +// ancestors might require custom translators however +func (t translator) translatable(t1, t2 reflect.Type) bool { + k1 := t1.Kind() + k2 := t2.Kind() + if k1 != k2 { + return false + } + switch { + case util.IsPrimitive(k1): + return true + case util.IsInvalidInConfig(k1): + panic(fmt.Sprintf("Encountered invalid kind %s in config. This is a bug, please file a report", k1)) + case k1 == reflect.Ptr || k1 == reflect.Slice: + return t.translatable(t1.Elem(), t2.Elem()) || t.hasTranslator(t1.Elem(), t2.Elem()) + case k1 == reflect.Struct: + return t.translatableStruct(t1, t2) + default: + panic(fmt.Sprintf("Encountered unknown kind %s in config. This is a bug, please file a report", k1)) + } +} + +// precondition: t1, t2 are both of Kind 'struct' +func (t translator) translatableStruct(t1, t2 reflect.Type) bool { + if t1.Name() != t2.Name() { + return false + } + t1Fields := 0 + for i := 0; i < t1.NumField(); i++ { + t1f := t1.Field(i) + if t1f.Tag.Get(TAG_KEY) == TAG_AUTO_SKIP { + // ignore this input field + continue + } + t1Fields++ + + t2f, ok := t2.FieldByName(t1f.Name) + if !ok { + return false + } + if !t.translatable(t1f.Type, t2f.Type) && !t.hasTranslator(t1f.Type, t2f.Type) { + return false + } + } + return t2.NumField() == t1Fields +} + +// checks that t could reasonably be the type of a translator function +func (t translator) couldBeValidTranslator(tr reflect.Type) bool { + if tr.Kind() != reflect.Func { + return false + } + if tr.NumIn() != 2 || tr.NumOut() != 3 { + return false + } + if util.IsInvalidInConfig(tr.In(0).Kind()) || + util.IsInvalidInConfig(tr.Out(0).Kind()) || + tr.In(1) != reflect.TypeOf(t.options) || + tr.Out(1) != translationsType || + tr.Out(2) != reportType { + return false + } + return true +} + +// translate from one type to another, but deep copy all data +// precondition: vFrom and vTo are the same type as defined by translatable() +// precondition: vTo is addressable and settable +func (t translator) translateSameType(vFrom, vTo reflect.Value, fromPath, toPath path.ContextPath) { + k := vFrom.Kind() + switch { + case util.IsPrimitive(k): + // Use convert, even if not needed; type alias to primitives are not + // directly assignable and calling Convert on primitives does no harm + vTo.Set(vFrom.Convert(vTo.Type())) + t.translations.AddTranslation(fromPath, toPath) + case k == reflect.Ptr: + if vFrom.IsNil() { + return + } + vTo.Set(reflect.New(vTo.Type().Elem())) + t.translate(vFrom.Elem(), vTo.Elem(), fromPath, toPath) + case k == reflect.Slice: + if vFrom.IsNil() { + return + } + vTo.Set(reflect.MakeSlice(vTo.Type(), vFrom.Len(), vFrom.Len())) + for i := 0; i < vFrom.Len(); i++ { + t.translate(vFrom.Index(i), vTo.Index(i), fromPath.Append(i), toPath.Append(i)) + } + t.translations.AddTranslation(fromPath, toPath) + case k == reflect.Struct: + for i := 0; i < vFrom.NumField(); i++ { + if vFrom.Type().Field(i).Tag.Get(TAG_KEY) == TAG_AUTO_SKIP { + // ignore this input field + continue + } + fieldGoName := vFrom.Type().Field(i).Name + toStructField, ok := vTo.Type().FieldByName(fieldGoName) + if !ok { + panic("vTo did not have a matching type. This is a bug; please file a report") + } + toFieldIndex := toStructField.Index[0] + vToField := vTo.FieldByName(fieldGoName) + + from := fromPath.Append(fieldName(vFrom, i, fromPath.Tag)) + to := toPath.Append(fieldName(vTo, toFieldIndex, toPath.Tag)) + if vFrom.Type().Field(i).Anonymous { + from = fromPath + to = toPath + } + t.translate(vFrom.Field(i), vToField, from, to) + } + if !vFrom.IsZero() { + t.translations.AddTranslation(fromPath, toPath) + } + default: + panic("Encountered types that are not the same when they should be. This is a bug, please file a report") + } +} + +// helper to return if a custom translator was defined +func (t translator) hasTranslator(tFrom, tTo reflect.Type) bool { + return t.getTranslator(tFrom, tTo).IsValid() +} + +// vTo must be addressable, should be acquired by calling reflect.ValueOf() on a variable of the correct type +func (t translator) translate(vFrom, vTo reflect.Value, fromPath, toPath path.ContextPath) { + tFrom := vFrom.Type() + tTo := vTo.Type() + if fnv := t.getTranslator(tFrom, tTo); fnv.IsValid() { + returns := fnv.Call([]reflect.Value{vFrom, reflect.ValueOf(t.options)}) + vTo.Set(returns[0]) + + // handle all the translations and "rebase" them to our current place + retSet := returns[1].Interface().(TranslationSet) + t.translations.Merge(retSet.PrefixPaths(fromPath, toPath)) + if len(retSet.Set) > 0 { + t.translations.AddTranslation(fromPath, toPath) + } + + // likewise for the report entries + retReport := returns[2].Interface().(report.Report) + for i := range retReport.Entries { + entry := &retReport.Entries[i] + entry.Context = fromPath.Append(entry.Context.Path...) + } + t.report.Merge(retReport) + return + } + if t.translatable(tFrom, tTo) { + t.translateSameType(vFrom, vTo, fromPath, toPath) + return + } + + panic(fmt.Sprintf("Translator not defined for %v to %v", tFrom, tTo)) +} + +type Translator interface { + // Adds a custom translator for cases where the structs are not identical. Must be of type + // func(fromType, optionsType) -> (toType, TranslationSet, report.Report). + // The translator should return the set of all translations it did. + AddCustomTranslator(t interface{}) + // Also returns a list of source and dest paths, autocompleted by fromTag and toTag + Translate(from, to interface{}) (TranslationSet, report.Report) +} + +// NewTranslator creates a new Translator for translating from types with fromTag struct tags (e.g. "yaml") +// to types with toTag struct tages (e.g. "json"). These tags are used when determining paths when generating +// the TranslationSet returned by Translator.Translate() +func NewTranslator(fromTag, toTag string, options interface{}) Translator { + return &translator{ + options: options, + translations: TranslationSet{ + FromTag: fromTag, + ToTag: toTag, + Set: map[string]Translation{}, + }, + } +} + +type translator struct { + options interface{} + // List of custom translation funcs, must pass couldBeValidTranslator + // This is only for fields that cannot or should not be trivially translated, + // All trivially translated fields use the default behavior. + translators []reflect.Value + translations TranslationSet + report *report.Report +} + +// fn should be of the form +// func(fromType, optionsType) -> (toType, TranslationSet, report.Report) +func (t *translator) AddCustomTranslator(fn interface{}) { + fnv := reflect.ValueOf(fn) + if !t.couldBeValidTranslator(fnv.Type()) { + panic("Tried to register invalid translator function") + } + t.translators = append(t.translators, fnv) +} + +func (t translator) getTranslator(from, to reflect.Type) reflect.Value { + for _, fn := range t.translators { + if fn.Type().In(0) == from && fn.Type().Out(0) == to { + return fn + } + } + return reflect.Value{} +} + +// Translate translates from into to and returns a set of all the path changes it performed. +func (t translator) Translate(from, to interface{}) (TranslationSet, report.Report) { + fv := reflect.ValueOf(from) + tv := reflect.ValueOf(to) + if fv.Kind() != reflect.Ptr || tv.Kind() != reflect.Ptr { + panic("Translate needs to be called on pointers") + } + fv = fv.Elem() + tv = tv.Elem() + // Make sure to clear these every time + t.translations = TranslationSet{ + FromTag: t.translations.FromTag, + ToTag: t.translations.ToTag, + Set: map[string]Translation{}, + } + t.report = &report.Report{} + t.translate(fv, tv, path.New(t.translations.FromTag), path.New(t.translations.ToTag)) + return t.translations, *t.report +} diff --git a/butane/translate/translate_test.go b/butane/translate/translate_test.go new file mode 100644 index 000000000..040305b46 --- /dev/null +++ b/butane/translate/translate_test.go @@ -0,0 +1,310 @@ +// 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 translate + +import ( + "errors" + "testing" + + "github.com/coreos/butane/translate/tests/pkga" + "github.com/coreos/butane/translate/tests/pkgb" + + "github.com/coreos/vcontext/report" + "github.com/stretchr/testify/assert" +) + +type testOptions struct{} + +// Note: we need different input and output types which unfortunately means a lot of tests + +func TestTranslateTrivial(t *testing.T) { + in := pkga.Trivial{ + A: "asdf", + B: 5, + C: true, + } + + expected := pkgb.Trivial{ + A: "asdf", + B: 5, + C: true, + } + exTrans := mkTrans( + fp(), fp(), + fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + ) + + got := pkgb.Trivial{} + + trans := NewTranslator("", "", testOptions{}) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestTranslateNested(t *testing.T) { + in := pkga.Nested{ + D: "foobar", + Trivial: pkga.Trivial{ + A: "asdf", + B: 5, + C: true, + }, + } + + expected := pkgb.Nested{ + D: "foobar", + Trivial: pkgb.Trivial{ + A: "asdf", + B: 5, + C: true, + }, + } + exTrans := mkTrans( + fp(), fp(), + fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + fp("D"), fp("D"), + ) + + got := pkgb.Nested{} + + trans := NewTranslator("", "", testOptions{}) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestTranslateTrivialReordered(t *testing.T) { + in := pkga.TrivialReordered{ + A: "asdf", + B: 5, + C: true, + } + + expected := pkgb.TrivialReordered{ + A: "asdf", + B: 5, + C: true, + } + exTrans := mkTrans( + fp(), fp(), + fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + ) + + got := pkgb.TrivialReordered{} + + trans := NewTranslator("", "", testOptions{}) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestTranslateTrivialSkip(t *testing.T) { + in := pkga.TrivialSkip{ + A: "asdf", + B: 5, + C: true, + } + + expected := pkgb.TrivialSkip{ + B: 5, + C: true, + } + exTrans := mkTrans( + fp(), fp(), + fp("B"), fp("B"), + fp("C"), fp("C"), + ) + + got := pkgb.TrivialSkip{} + + trans := NewTranslator("", "", testOptions{}) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestCustomTranslatorTrivial(t *testing.T) { + tr := func(a pkga.Trivial, options testOptions) (pkgb.Nested, TranslationSet, report.Report) { + ts := mkTrans(fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + fp("C"), fp("D"), + ) + var r report.Report + r.AddOnInfo(fp("A"), errors.New("info")) + return pkgb.Nested{ + Trivial: pkgb.Trivial{ + A: a.A, + B: a.B, + C: a.C, + }, + D: "abc", + }, ts, r + } + in := pkga.Trivial{ + A: "asdf", + B: 5, + C: true, + } + + expected := pkgb.Nested{ + D: "abc", + Trivial: pkgb.Trivial{ + A: "asdf", + B: 5, + C: true, + }, + } + exTrans := mkTrans( + fp(), fp(), + fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + fp("C"), fp("D"), + ) + + got := pkgb.Nested{} + + trans := NewTranslator("", "", testOptions{}) + trans.AddCustomTranslator(tr) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "info at $.A: info\n", "bad report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestCustomTranslatorTrivialWithAutomaticResume(t *testing.T) { + trans := NewTranslator("", "", testOptions{}) + tr := func(a pkga.Trivial, options testOptions) (pkgb.Nested, TranslationSet, report.Report) { + ret := pkgb.Nested{ + D: "abc", + } + ts, r := trans.Translate(&a, &ret.Trivial) + ts.AddTranslation(fp("C"), fp("D")) + return ret, ts, r + } + in := pkga.Trivial{ + A: "asdf", + B: 5, + C: true, + } + exTrans := mkTrans( + fp(), fp(), + fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + fp("C"), fp("D"), + ) + + expected := pkgb.Nested{ + D: "abc", + Trivial: pkgb.Trivial{ + A: "asdf", + B: 5, + C: true, + }, + } + + got := pkgb.Nested{} + + trans.AddCustomTranslator(tr) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} + +func TestCustomTranslatorList(t *testing.T) { + tr := func(a pkga.Trivial, options testOptions) (pkgb.Nested, TranslationSet, report.Report) { + ts := mkTrans(fp("A"), fp("A"), + fp("B"), fp("B"), + fp("C"), fp("C"), + fp("C"), fp("D"), + ) + return pkgb.Nested{ + Trivial: pkgb.Trivial{ + A: a.A, + B: a.B, + C: a.C, + }, + D: "abc", + }, ts, report.Report{} + } + in := pkga.HasList{ + L: []pkga.Trivial{ + { + A: "asdf", + B: 5, + C: true, + }, + }, + } + + expected := pkgb.HasList{ + L: []pkgb.Nested{ + { + D: "abc", + Trivial: pkgb.Trivial{ + A: "asdf", + B: 5, + C: true, + }, + }, + }, + } + exTrans := mkTrans( + fp(), fp(), + fp("L"), fp("L"), + fp("L", 0), fp("L", 0), + fp("L", 0, "A"), fp("L", 0, "A"), + fp("L", 0, "B"), fp("L", 0, "B"), + fp("L", 0, "C"), fp("L", 0, "C"), + fp("L", 0, "C"), fp("L", 0, "D"), + ) + + got := pkgb.HasList{} + + trans := NewTranslator("", "", testOptions{}) + trans.AddCustomTranslator(tr) + + ts, r := trans.Translate(&in, &got) + assert.Equal(t, got, expected, "bad translation") + assert.Equal(t, ts, exTrans, "bad translation") + assert.Equal(t, r.String(), "", "non-empty report") + assert.NoError(t, ts.DebugVerifyCoverage(&got), "incomplete TranslationSet coverage") +} diff --git a/butane/translate/util.go b/butane/translate/util.go new file mode 100644 index 000000000..71f0b40e8 --- /dev/null +++ b/butane/translate/util.go @@ -0,0 +1,133 @@ +// 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 translate + +import ( + "reflect" + "strings" + + "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/vcontext/path" + "github.com/coreos/vcontext/report" +) + +// fieldName returns the name uses when (un)marshalling a field. t should be a reflect.Value of a struct, +// index is the field index, and tag is the struct tag used when (un)marshalling (e.g. "json" or "yaml") +func fieldName(t reflect.Value, index int, tag string) string { + f := t.Type().Field(index) + if tag == "" { + return f.Name + } + return strings.Split(f.Tag.Get(tag), ",")[0] +} + +func prefixPath(p path.ContextPath, prefix ...interface{}) path.ContextPath { + return path.New(p.Tag, prefix...).Append(p.Path...) +} + +func prefixPaths(ps []path.ContextPath, prefix ...interface{}) []path.ContextPath { + ret := []path.ContextPath{} + for _, p := range ps { + ret = append(ret, prefixPath(p, prefix...)) + } + return ret +} + +func getAllPaths(v reflect.Value, tag string, includeZeroFields bool) []path.ContextPath { + k := v.Kind() + t := v.Type() + switch { + case util.IsPrimitive(k): + return nil + case k == reflect.Ptr: + if v.IsNil() { + return nil + } + return getAllPaths(v.Elem(), tag, includeZeroFields) + case k == reflect.Slice: + ret := []path.ContextPath{} + for i := 0; i < v.Len(); i++ { + // for struct, pointer to struct, etc., add any children + ret = append(ret, prefixPaths(getAllPaths(v.Index(i), tag, includeZeroFields), i)...) + // add slice entry + ret = append(ret, path.New(tag, i)) + } + return ret + case k == reflect.Struct: + ret := []path.ContextPath{} + for i := 0; i < t.NumField(); i++ { + name := fieldName(v, i, tag) + field := v.Field(i) + if !includeZeroFields && field.IsZero() { + continue + } + if t.Field(i).Anonymous { + ret = append(ret, getAllPaths(field, tag, includeZeroFields)...) + } else { + ret = append(ret, prefixPaths(getAllPaths(field, tag, includeZeroFields), name)...) + ret = append(ret, path.New(tag, name)) + } + } + return ret + case k == reflect.Map: + // we don't have these in Butane or Ignition configs, but + // we need to support validating translations of + // metadata.labels in MachineConfig output + ret := []path.ContextPath{} + iter := v.MapRange() + for iter.Next() { + // for struct, pointer to struct, etc., add any children + ret = append(ret, prefixPaths(getAllPaths(iter.Value(), tag, includeZeroFields), iter.Key())...) + // add map entry + ret = append(ret, path.New(tag, iter.Key())) + } + return ret + default: + panic("Encountered unexpected type. This is a bug, please file a report") + } +} + +// Return a copy of the report, with the context paths prefixed by prefix. +func PrefixReport(r report.Report, prefix interface{}) report.Report { + var ret report.Report + ret.Merge(r) + for i := range ret.Entries { + entry := &ret.Entries[i] + entry.Context = path.New(entry.Context.Tag, prefix).Append(entry.Context.Path...) + } + return ret +} + +// Utility function to run a translation and prefix the resulting +// TranslationSet and Report. +func Prefixed(tr Translator, prefix interface{}, from interface{}, to interface{}) (TranslationSet, report.Report) { + tm, r := tr.Translate(from, to) + return tm.Prefix(prefix), PrefixReport(r, prefix) +} + +// Utility function to run a translation and merge the result, with the +// specified prefix, into the specified TranslationSet and Report. +func MergeP(tr Translator, tm TranslationSet, r *report.Report, prefix interface{}, from interface{}, to interface{}) { + MergeP2(tr, tm, r, prefix, from, prefix, to) +} + +// Utility function to run a translation and merge the result, with the +// specified prefixes, into the specified TranslationSet and Report. +func MergeP2(tr Translator, tm TranslationSet, r *report.Report, fromPrefix interface{}, from interface{}, toPrefix interface{}, to interface{}) { + translations, translationReport := tr.Translate(from, to) + tm.MergeP2(fromPrefix, toPrefix, translations) + // translation report paths are on the from side + r.Merge(PrefixReport(translationReport, fromPrefix)) +} diff --git a/butane/vendor/github.com/aws/aws-sdk-go-v2/LICENSE.txt b/butane/vendor/github.com/aws/aws-sdk-go-v2/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/butane/vendor/github.com/aws/aws-sdk-go-v2/LICENSE.txt @@ -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/vendor/github.com/aws/aws-sdk-go-v2/NOTICE.txt b/butane/vendor/github.com/aws/aws-sdk-go-v2/NOTICE.txt new file mode 100644 index 000000000..899129ecc --- /dev/null +++ b/butane/vendor/github.com/aws/aws-sdk-go-v2/NOTICE.txt @@ -0,0 +1,3 @@ +AWS SDK for Go +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2014-2015 Stripe, Inc. diff --git a/butane/vendor/github.com/aws/aws-sdk-go-v2/aws/arn/arn.go b/butane/vendor/github.com/aws/aws-sdk-go-v2/aws/arn/arn.go new file mode 100644 index 000000000..fe63fedad --- /dev/null +++ b/butane/vendor/github.com/aws/aws-sdk-go-v2/aws/arn/arn.go @@ -0,0 +1,92 @@ +// Package arn provides a parser for interacting with Amazon Resource Names. +package arn + +import ( + "errors" + "strings" +) + +const ( + arnDelimiter = ":" + arnSections = 6 + arnPrefix = "arn:" + + // zero-indexed + sectionPartition = 1 + sectionService = 2 + sectionRegion = 3 + sectionAccountID = 4 + sectionResource = 5 + + // errors + invalidPrefix = "arn: invalid prefix" + invalidSections = "arn: not enough sections" +) + +// ARN captures the individual fields of an Amazon Resource Name. +// See http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html for more information. +type ARN struct { + // The partition that the resource is in. For standard AWS regions, the partition is "aws". If you have resources in + // other partitions, the partition is "aws-partitionname". For example, the partition for resources in the China + // (Beijing) region is "aws-cn". + Partition string + + // The service namespace that identifies the AWS product (for example, Amazon S3, IAM, or Amazon RDS). For a list of + // namespaces, see + // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces. + Service string + + // The region the resource resides in. Note that the ARNs for some resources do not require a region, so this + // component might be omitted. + Region string + + // The ID of the AWS account that owns the resource, without the hyphens. For example, 123456789012. Note that the + // ARNs for some resources don't require an account number, so this component might be omitted. + AccountID string + + // The content of this part of the ARN varies by service. It often includes an indicator of the type of resource — + // for example, an IAM user or Amazon RDS database - followed by a slash (/) or a colon (:), followed by the + // resource name itself. Some services allows paths for resource names, as described in + // http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-paths. + Resource string +} + +// Parse parses an ARN into its constituent parts. +// +// Some example ARNs: +// arn:aws:elasticbeanstalk:us-east-1:123456789012:environment/My App/MyEnvironment +// arn:aws:iam::123456789012:user/David +// arn:aws:rds:eu-west-1:123456789012:db:mysql-db +// arn:aws:s3:::my_corporate_bucket/exampleobject.png +func Parse(arn string) (ARN, error) { + if !strings.HasPrefix(arn, arnPrefix) { + return ARN{}, errors.New(invalidPrefix) + } + sections := strings.SplitN(arn, arnDelimiter, arnSections) + if len(sections) != arnSections { + return ARN{}, errors.New(invalidSections) + } + return ARN{ + Partition: sections[sectionPartition], + Service: sections[sectionService], + Region: sections[sectionRegion], + AccountID: sections[sectionAccountID], + Resource: sections[sectionResource], + }, nil +} + +// IsARN returns whether the given string is an arn +// by looking for whether the string starts with arn: +func IsARN(arn string) bool { + return strings.HasPrefix(arn, arnPrefix) && strings.Count(arn, ":") >= arnSections-1 +} + +// String returns the canonical representation of the ARN +func (arn ARN) String() string { + return arnPrefix + + arn.Partition + arnDelimiter + + arn.Service + arnDelimiter + + arn.Region + arnDelimiter + + arn.AccountID + arnDelimiter + + arn.Resource +} diff --git a/butane/vendor/github.com/clarketm/json/LICENSE b/butane/vendor/github.com/clarketm/json/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/butane/vendor/github.com/clarketm/json/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/butane/vendor/github.com/clarketm/json/README.md b/butane/vendor/github.com/clarketm/json/README.md new file mode 100644 index 000000000..e5c3524a9 --- /dev/null +++ b/butane/vendor/github.com/clarketm/json/README.md @@ -0,0 +1,19 @@ +# [json](https://godoc.org/github.com/clarketm/json) +> Mirrors [golang/go](https://github.com/golang/go) [![Golang version](https://img.shields.io/badge/go-1.12.7-green)](https://github.com/golang/go/releases/tag/go1.12.7) + +Drop-in replacement for Golang [`encoding/json`](https://golang.org/pkg/encoding/json/) with additional features. + +## Installation +```shell +$ go get -u github.com/clarketm/json +``` + +## Usage +Same usage as Golang [`encoding/json`](https://golang.org/pkg/encoding/json/). + +## Features +- Support zero values of structs with `omitempty`: [golang/go#11939](https://github.com/golang/go/issues/11939). +> If `omitempty` is applied to a struct and all the children of the struct are *empty*, then on marshalling it will be **omitted** from the encoded json. + +## License +Refer to the [Golang](https://github.com/golang/go/blob/master/LICENSE) license. See [LICENSE](LICENSE) for more information. diff --git a/butane/vendor/github.com/clarketm/json/decode.go b/butane/vendor/github.com/clarketm/json/decode.go new file mode 100644 index 000000000..a9917e72c --- /dev/null +++ b/butane/vendor/github.com/clarketm/json/decode.go @@ -0,0 +1,1310 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "encoding" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an InvalidUnmarshalError. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a value implementing the Unmarshaler interface, +// Unmarshal calls that value's UnmarshalJSON method, including +// when the input is a JSON null. +// Otherwise, if the value implements encoding.TextUnmarshaler +// and the input is a JSON quoted string, Unmarshal calls that value's +// UnmarshalText method with the unquoted form of the string. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. By +// default, object keys which don't have a corresponding struct field are +// ignored (see Decoder.DisallowUnknownFields for an alternative). +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a map, Unmarshal first establishes a map to +// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal +// reuses the existing map, keeping existing entries. Unmarshal then stores +// key-value pairs from the JSON object into the map. The map's key type must +// either be any string type, an integer, implement json.Unmarshaler, or +// implement encoding.TextUnmarshaler. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. In any +// case, it's not guaranteed that all the remaining fields following +// the problematic one will be unmarshaled into the target object. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// ``not present,'' unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +// +func Unmarshal(data []byte, v interface{}) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by types +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +// +// By convention, to approximate the behavior of Unmarshal itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes + Struct string // name of the struct type containing the field + Field string // the full path from root node to the field +} + +func (e *UnmarshalTypeError) Error() string { + if e.Struct != "" || e.Field != "" { + return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + } + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// +// Deprecated: No longer used; kept for compatibility. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v interface{}) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + d.scanWhile(scanSkipSpace) + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + err := d.value(rv) + if err != nil { + return d.addErrorContext(err) + } + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// An errorContext provides context for type errors during decoding. +type errorContext struct { + Struct reflect.Type + FieldStack []string +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // next read offset in data + opcode int // last read result + scan scanner + errorContext *errorContext + savedError error + useNumber bool + disallowUnknownFields bool +} + +// readIndex returns the position of the last byte read. +func (d *decodeState) readIndex() int { + return d.off - 1 +} + +// phasePanicMsg is used as a panic message when we end up with something that +// shouldn't happen. It can indicate a bug in the JSON decoder, or that +// something is editing the data slice while the decoder executes. +const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + if d.errorContext != nil { + d.errorContext.Struct = nil + // Reuse the allocated space for the FieldStack slice. + d.errorContext.FieldStack = d.errorContext.FieldStack[:0] + } + return d +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = d.addErrorContext(err) + } +} + +// addErrorContext returns a new error enhanced with information from d.errorContext +func (d *decodeState) addErrorContext(err error) error { + if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) { + switch err := err.(type) { + case *UnmarshalTypeError: + err.Struct = d.errorContext.Struct.Name() + err.Field = strings.Join(d.errorContext.FieldStack, ".") + } + } + return err +} + +// skip scans to the end of what was started. +func (d *decodeState) skip() { + s, data, i := &d.scan, d.data, d.off + depth := len(s.parseState) + for { + op := s.step(s, data[i]) + i++ + if len(s.parseState) < depth { + d.off = i + d.opcode = op + return + } + } +} + +// scanNext processes the byte at d.data[d.off]. +func (d *decodeState) scanNext() { + if d.off < len(d.data) { + d.opcode = d.scan.step(&d.scan, d.data[d.off]) + d.off++ + } else { + d.opcode = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +func (d *decodeState) scanWhile(op int) { + s, data, i := &d.scan, d.data, d.off + for i < len(data) { + newOp := s.step(s, data[i]) + i++ + if newOp != op { + d.opcode = newOp + d.off = i + return + } + } + + d.off = len(data) + 1 // mark processed EOF with len+1 + d.opcode = d.scan.eof() +} + +// rescanLiteral is similar to scanWhile(scanContinue), but it specialises the +// common case where we're decoding a literal. The decoder scans the input +// twice, once for syntax errors and to check the length of the value, and the +// second to perform the decoding. +// +// Only in the second step do we use decodeState to tokenize literals, so we +// know there aren't any syntax errors. We can take advantage of that knowledge, +// and scan a literal's bytes much more quickly. +func (d *decodeState) rescanLiteral() { + data, i := d.data, d.off +Switch: + switch data[i-1] { + case '"': // string + for ; i < len(data); i++ { + switch data[i] { + case '\\': + i++ // escaped char + case '"': + i++ // tokenize the closing quote too + break Switch + } + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number + for ; i < len(data); i++ { + switch data[i] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', 'e', 'E', '+', '-': + default: + break Switch + } + } + case 't': // true + i += len("rue") + case 'f': // false + i += len("alse") + case 'n': // null + i += len("ull") + } + if i < len(data) { + d.opcode = stateEndValue(&d.scan, data[i]) + } else { + d.opcode = scanEnd + } + d.off = i + 1 +} + +// value consumes a JSON value from d.data[d.off-1:], decoding into v, and +// reads the following byte ahead. If v is invalid, the value is discarded. +// The first byte of the value has been read already. +func (d *decodeState) value(v reflect.Value) error { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray: + if v.IsValid() { + if err := d.array(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginObject: + if v.IsValid() { + if err := d.object(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginLiteral: + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + if v.IsValid() { + if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil { + return err + } + } + } + return nil +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() interface{} { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray, scanBeginObject: + d.skip() + d.scanNext() + + case scanBeginLiteral: + v := d.literalInterface() + switch v.(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// If it encounters an Unmarshaler, indirect stops and returns that. +// If decodingNull is true, indirect stops at the first settable pointer so it +// can be set to nil. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // Issue #24153 indicates that it is generally not a guaranteed property + // that you may round-trip a reflect.Value by calling Value.Addr().Elem() + // and expect the value to still be settable for values derived from + // unexported embedded struct fields. + // + // The logic below effectively does this when it first addresses the value + // (to satisfy possible pointer methods) and continues to dereference + // subsequent pointers as necessary. + // + // After the first round-trip, we set v back to the original value to + // preserve the original RW flags contained in reflect.Value. + v0 := v + haveAddr := false + + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + haveAddr = true + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + haveAddr = false + v = e + continue + } + } + + if v.Kind() != reflect.Ptr { + break + } + + if decodingNull && v.CanSet() { + break + } + + // Prevent infinite loop if v is an interface pointing to its own address: + // var v interface{} + // v = &v + if v.Elem().Kind() == reflect.Interface && v.Elem().Elem() == v { + v = v.Elem() + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if !decodingNull { + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + } + + if haveAddr { + v = v0 // restore original value after round-trip Value.Addr().Elem() + haveAddr = false + } else { + v = v.Elem() + } + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into v. +// The first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + ai := d.arrayInterface() + v.Set(reflect.ValueOf(ai)) + return nil + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + case reflect.Array, reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + if err := d.value(v.Index(i)); err != nil { + return err + } + } else { + // Ran out of fixed array: skip. + if err := d.value(reflect.Value{}); err != nil { + return err + } + } + i++ + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +var nullLiteral = []byte("null") +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + +// object consumes an object from d.data[d.off-1:], decoding into v. +// The first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + t := v.Type() + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + oi := d.objectInterface() + v.Set(reflect.ValueOf(oi)) + return nil + } + + var fields structFields + + // Check type of target: + // struct or + // map[T1]T2 where T1 is string, an integer type, + // or an encoding.TextUnmarshaler + switch v.Kind() { + case reflect.Map: + // Map key must either have string kind, have an integer kind, + // or be an encoding.TextUnmarshaler. + switch t.Key().Kind() { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + default: + if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) { + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + fields = cachedTypeFields(t) + // ok + default: + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + + var mapElem reflect.Value + var origErrorContext errorContext + if d.errorContext != nil { + origErrorContext = *d.errorContext + } + + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquoteBytes(item) + if !ok { + panic(phasePanicMsg) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := t.Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.Set(reflect.Zero(elemType)) + } + subv = mapElem + } else { + var f *field + if i, ok := fields.nameIndex[string(key)]; ok { + // Found an exact name match. + f = &fields.list[i] + } else { + // Fall back to the expensive case-insensitive + // linear search. + for i := range fields.list { + ff := &fields.list[i] + if ff.equalFold(ff.nameBytes, key) { + f = ff + break + } + } + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Ptr { + if subv.IsNil() { + // If a struct embeds a pointer to an unexported type, + // it is not possible to set a newly allocated value + // since the field is unexported. + // + // See https://golang.org/issue/21357 + if !subv.CanSet() { + d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem())) + // Invalidate subv to ensure d.value(subv) skips over + // the JSON value without assigning it to subv. + subv = reflect.Value{} + destring = false + break + } + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + if d.errorContext == nil { + d.errorContext = new(errorContext) + } + d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name) + d.errorContext.Struct = t + } else if d.disallowUnknownFields { + d.saveError(fmt.Errorf("json: unknown field %q", key)) + } + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + if err := d.literalStore(nullLiteral, subv, false); err != nil { + return err + } + case string: + if err := d.literalStore([]byte(qv), subv, true); err != nil { + return err + } + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + if err := d.value(subv); err != nil { + return err + } + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kt := t.Key() + var kv reflect.Value + switch { + case reflect.PtrTo(kt).Implements(textUnmarshalerType): + kv = reflect.New(kt) + if err := d.literalStore(item, kv, true); err != nil { + return err + } + kv = kv.Elem() + case kt.Kind() == reflect.String: + kv = reflect.ValueOf(key).Convert(kt) + default: + switch kt.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := string(key) + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := string(key) + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + default: + panic("json: Unexpected key type") // should never occur + } + } + if kv.IsValid() { + v.SetMapIndex(kv, subv) + } + } + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.errorContext != nil { + // Reset errorContext to its original state. + // Keep the same underlying array for FieldStack, to reuse the + // space and avoid unnecessary allocs. + d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)] + d.errorContext.Struct = origErrorContext.Struct + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return nil +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (interface{}, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} + } + return f, nil +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error { + // Check for unmarshaler. + if len(item) == 0 { + //Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + isNull := item[0] == 'n' // null + u, ut, pv := indirect(v, isNull) + if u != nil { + return u.UnmarshalJSON(item) + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + val := "number" + switch item[0] { + case 'n': + val = "null" + case 't', 'f': + val = "bool" + } + d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex())}) + return nil + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + return ut.UnmarshalText(s) + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "null" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + v.Set(reflect.Zero(v.Type())) + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := item[0] == 't' + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "true" && string(item) != "false" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + if v.Type() == numberType && !isValidNumber(string(s)) { + return fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item) + } + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + // s must be a valid number, because it's + // already been tokenized. + v.SetString(s) + break + } + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetFloat(n) + } + } + return nil +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() (val interface{}) { + switch d.opcode { + default: + panic(phasePanicMsg) + case scanBeginArray: + val = d.arrayInterface() + d.scanNext() + case scanBeginObject: + val = d.objectInterface() + d.scanNext() + case scanBeginLiteral: + val = d.literalInterface() + } + return +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []interface{} { + var v = make([]interface{}, 0) + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]interface{} { + m := make(map[string]interface{}) + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read string key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return m +} + +// literalInterface consumes and returns a literal from d.data[d.off-1:] and +// it reads the following byte ahead. The first byte of the literal has been +// read already (that's how the caller knows it's a literal). +func (d *decodeState) literalInterface() interface{} { + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + item := d.data[start:d.readIndex()] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + panic(phasePanicMsg) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/butane/vendor/github.com/clarketm/json/encode.go b/butane/vendor/github.com/clarketm/json/encode.go new file mode 100644 index 000000000..06b2f754c --- /dev/null +++ b/butane/vendor/github.com/clarketm/json/encode.go @@ -0,0 +1,1430 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON as defined in +// RFC 7159. The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// So that the JSON will be safe to embed inside HTML