diff --git a/docs/release-notes.md b/docs/release-notes.md index 95d3ad5a66..84cc7b7be6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,8 @@ nav_order: 9 ### Changes +- `./test` validates the Butane configs in `butane/docs`, restoring coverage that was lost when Butane merged into this repository + ### Bug fixes - Resolve intermediate symlinks in relabel paths, fixing SELinux relabeling failures for users with `home_dir` on OSTree platforms after policycoreutils 3.11 ([#2316](https://github.com/coreos/ignition/pull/2316)) diff --git a/internal/util/tools/docs/docs.go b/internal/util/tools/docs/docs.go index 11731ada7c..ccc09e3a42 100644 --- a/internal/util/tools/docs/docs.go +++ b/internal/util/tools/docs/docs.go @@ -13,7 +13,7 @@ // limitations under the License. // Reads all markdown files in the specified directory and validates the -// Ignition configs wrapped in code fences. +// Ignition and Butane configs wrapped in code fences. package main @@ -24,18 +24,50 @@ import ( "path/filepath" "strings" - "github.com/coreos/ignition/v2/config" + butane "github.com/coreos/ignition/v2/butane/config" + butanecommon "github.com/coreos/ignition/v2/butane/config/common" + ignition "github.com/coreos/ignition/v2/config" ) -// Specific section marker used in the docs to indicate that the Markdown code -// section right after it should be treated as being a valid Ignition config -// and thus used for testing. +// Specific section markers used in the docs to indicate that the Markdown code +// section right after one should be treated as being a valid config and thus +// used for testing. const ( - sectionMarker = "" + ignitionMarker = "" + butaneMarker = "" ) -// Represent the state we are in while trying to extract Ignition config -// sections from the examples in the docs. +// The kind of config a section holds, which determines both the code fence it +// must use and how it is validated. +type configKind int + +const ( + ignitionConfig configKind = iota + butaneConfig +) + +func (k configKind) fence() string { + if k == butaneConfig { + return "```yaml" + } + return "```json" +} + +func (k configKind) String() string { + if k == butaneConfig { + return "Butane" + } + return "Ignition" +} + +// A config section extracted from a Markdown file. +type configSection struct { + kind configKind + lines []string +} + +// Represent the state we are in while trying to extract config sections from +// the examples in the docs. type sectionState int const ( @@ -46,12 +78,14 @@ const ( func main() { flags := struct { - help bool - root string + help bool + root string + filesDir string }{} flag.BoolVar(&flags.help, "help", false, "Print help and exit.") flag.StringVar(&flags.root, "root", "docs", "Path to the documentation.") + flag.StringVar(&flags.filesDir, "files-dir", "", "Directory Butane configs may embed local files from.") flag.Parse() @@ -75,28 +109,20 @@ func main() { } fileLines := strings.Split(string(fileContents), "\n") - jsonSections, ignored, err := findJsonSections(fileLines) + sections, ignored, err := findConfigSections(fileLines) if err != nil { return fmt.Errorf("invalid section formatting in %s: %s", path, err) } - if len(jsonSections) != 0 { - fmt.Printf("Found %d sections in: %s\n", len(jsonSections), path) + if len(sections) != 0 { + fmt.Printf("Found %d sections in: %s\n", len(sections), path) } if ignored != 0 { fmt.Printf("Ignored %d partial or empty sections in: %s\n", ignored, path) } - for _, json := range jsonSections { - cfg := strings.Join(json, "\n") - _, r, err := config.Parse([]byte(cfg)) - // the report provides a more specific error - // description, so check that first - reportStr := r.String() - if reportStr != "" { - return fmt.Errorf("non-empty parsing report in %s: %s\nConfig:\n%s", info.Name(), reportStr, cfg) - } - if err != nil { - return fmt.Errorf("fatal error parsing %s: %s\nConfig:\n%s", info.Name(), err, cfg) + for _, section := range sections { + if err := validateSection(section, info.Name(), flags.filesDir); err != nil { + return err } } @@ -107,9 +133,45 @@ func main() { } } -func findJsonSections(fileLines []string) ([][]string, uint, error) { - var jsonSections [][]string +// validateSection checks one config section, matching the strictness of +// `ignition-validate` and of `butane --check --strict` respectively. +func validateSection(section configSection, name, filesDir string) error { + cfg := strings.Join(section.lines, "\n") + + switch section.kind { + case butaneConfig: + _, r, err := butane.TranslateBytes([]byte(cfg), butanecommon.TranslateBytesOptions{ + TranslateOptions: butanecommon.TranslateOptions{ + FilesDir: filesDir, + }, + }) + if err != nil { + return fmt.Errorf("fatal error translating %s: %s\nConfig:\n%s", name, err, cfg) + } + // `--strict` treats any report entry, warnings included, as fatal + if len(r.Entries) > 0 { + return fmt.Errorf("non-empty translation report in %s: %s\nConfig:\n%s", name, r.String(), cfg) + } + default: + _, r, err := ignition.Parse([]byte(cfg)) + // the report provides a more specific error + // description, so check that first + reportStr := r.String() + if reportStr != "" { + return fmt.Errorf("non-empty parsing report in %s: %s\nConfig:\n%s", name, reportStr, cfg) + } + if err != nil { + return fmt.Errorf("fatal error parsing %s: %s\nConfig:\n%s", name, err, cfg) + } + } + + return nil +} + +func findConfigSections(fileLines []string) ([]configSection, uint, error) { + var sections []configSection var currentSection []string + var currentKind configKind var ignoredSections uint = 0 var state = notInSection @@ -117,15 +179,20 @@ func findJsonSections(fileLines []string) ([][]string, uint, error) { for _, line := range fileLines { switch state { case notInSection: - if line == sectionMarker { + switch line { + case ignitionMarker: + currentKind = ignitionConfig + state = expectingSection + case butaneMarker: + currentKind = butaneConfig state = expectingSection } case expectingSection: - if line == "```json" { + if line == currentKind.fence() { state = inSection } else { - return jsonSections, ignoredSections, fmt.Errorf("expecting '```json', found: %s", line) + return sections, ignoredSections, fmt.Errorf("expecting '%s', found: %s", currentKind.fence(), line) } case inSection: @@ -134,7 +201,10 @@ func findJsonSections(fileLines []string) ([][]string, uint, error) { // Ignore empty sections and sections that are not full configs ignoredSections++ } else { - jsonSections = append(jsonSections, currentSection) + sections = append(sections, configSection{ + kind: currentKind, + lines: currentSection, + }) } currentSection = nil state = notInSection @@ -143,5 +213,15 @@ func findJsonSections(fileLines []string) ([][]string, uint, error) { } } } - return jsonSections, ignoredSections, nil + + // A file ending mid-section would otherwise drop that section and report + // success, silently skipping validation of it. + switch state { + case expectingSection: + return sections, ignoredSections, fmt.Errorf("expecting '%s' after %s marker, found end of file", currentKind.fence(), currentKind) + case inSection: + return sections, ignoredSections, fmt.Errorf("unterminated %s config section", currentKind) + } + + return sections, ignoredSections, nil } diff --git a/test b/test index 1fe2a233fe..a4312e5851 100755 --- a/test +++ b/test @@ -75,6 +75,18 @@ fi echo "Checking docs..." go run internal/util/tools/docs/docs.go + +echo "Checking Butane docs..." +# Butane configs in the docs embed local files; supply the ones they reference +# so translation fails on a broken config rather than on a missing file. +butane_files_dir=$(mktemp -d) +trap 'rm -rf "${butane_files_dir}"' EXIT +mkdir -p "${butane_files_dir}/tree" +touch "${butane_files_dir}"/{config.ign,ca.pem,example.conf,example.service,file,file-epilogue,local-file3} +echo "ssh-rsa AAAA" > "${butane_files_dir}/id_rsa.pub" +echo "ssh-ed25519 AAAA" > "${butane_files_dir}/id_ed25519.pub" +echo '{"ignition": {"version": "3.5.0"}}' > "${butane_files_dir}/ignition.ign" +go run internal/util/tools/docs/docs.go -root butane/docs -files-dir "${butane_files_dir}" # Ensure every platform is listed in supported-platforms.md platforms=$(grep -A 1 -h platform.Register internal/providers/*/* | grep Name: | cut -f2 -d\") if [ -z "${platforms}" ]; then @@ -101,7 +113,7 @@ if [ ! -x "${BUTANE_BIN}" ]; then fi shopt -s nullglob tmpdir=$(mktemp -d) -trap 'rm -rf "${tmpdir}"' EXIT +trap 'rm -rf "${butane_files_dir}" "${tmpdir}"' EXIT # Create files-dir contents expected by example configs mkdir -p "${tmpdir}/files-dir/tree" touch "${tmpdir}/files-dir/"{config.ign,ca.pem,example.conf,example.service,file,file-epilogue,local-file3}