Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
142 changes: 111 additions & 31 deletions internal/util/tools/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 = "<!-- ignition -->"
ignitionMarker = "<!-- ignition -->"
butaneMarker = "<!-- butane-config -->"
)

// 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 (
Expand All @@ -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()

Expand All @@ -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
}
}

Expand All @@ -107,25 +133,66 @@ 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

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:
Expand All @@ -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
Expand All @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
14 changes: 13 additions & 1 deletion test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand Down