Skip to content

Commit

Permalink
preprocessor: add execute --check flag to validate page CUE
Browse files Browse the repository at this point in the history
Our "mode" of loading the configuration for the cuelang.org site
involves globbing CUE files found at page roots and presenting all those
files to cue/load.Instances. The issue tracking adding more standard
modes to cue/load is https://cuelang.org/issue/2018.

Our mode of loading also seeks to enforce that a page rooted at
content/a/b only declares CUE values under that path:

    content: a: b: {
        // ...
    }

This commit adds a --check flag to the execute command that enforces
this. It also adds this check to CI to enforce it.

Preprocessor-No-Write-Cache: true
Signed-off-by: Paul Jolly <[email protected]>
Change-Id: Id963877e586b230f221b2e7843dc48471e86d1b7
Dispatch-Trailer: {"type":"trybot","CL":1170108,"patchset":3,"ref":"refs/changes/08/1170108/3","targetBranch":"alpha"}
  • Loading branch information
myitcv authored and cueckoo committed Sep 30, 2023
1 parent 3dd139c commit ed7100e
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 0 deletions.
1 change: 1 addition & 0 deletions internal/cmd/preprocessor/cmd/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
flagSkipCache flagName = "skipcache"
flagHugoFlag flagName = "hugo"
flagNoWriteCache flagName = "nowritecache"
flagCheck flagName = "check"
)

var (
Expand Down
88 changes: 88 additions & 0 deletions internal/cmd/preprocessor/cmd/execute_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"runtime/debug"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
preprocessembed "github.com/cue-lang/cuelang.org"
)
Expand Down Expand Up @@ -79,6 +80,14 @@ func (ec *executeContext) execute() error {
return err
}

// If we have been given the --check flag, ensure that the pages
// we found have CUE files that are correctly namespaced.
if flagCheck.Bool(ec.executor.cmd) {
if ec.checkPageCUE(); ec.isInError() {
return errorIfInError(ec)
}
}

// Load all the CUE in one go
cfg := &load.Config{
Dir: ec.executor.root,
Expand Down Expand Up @@ -138,6 +147,85 @@ func (ec *executeContext) execute() error {
return errorIfInError(ec)
}

// checkPageCUE checks that the CUE files found in each page root directory
// is well namespaced for the directory in which is is contained.
//
// For example, for content/docs/howto/find-a-guide/page.cue it will ensure
// that fields are defined only within the content.docs.howto."find-a-guide"
// struct.
func (ec *executeContext) checkPageCUE() {
dirs:
for _, absDir := range ec.order {
// Load the files in the directory (assuming they all belong
// to the same package)
var filenames []string
filenames, err := filepath.Glob(filepath.Join(absDir, "*.cue"))
if err != nil {
ec.errorf("%s: failed to read: %v", absDir, err)
continue
}

bps := load.Instances(filenames, &load.Config{
Dir: absDir,
})
if l := len(bps); l != 1 {
ec.errorf("%s: expected 1 build package; saw %d", absDir, l)
continue
}
v := ec.ctx.BuildInstance(bps[0])

// If we have an error at this stage we can't be
// sure things are fine. Bail early
if err := v.Err(); err != nil {
ec.errorf("%s: error loading .cue files: %v", absDir, err)
continue
}

// derive the relative dirPath of d to the root, in canonical dirPath format
// (i.e. not OS-specific)
relDir := strings.TrimPrefix(absDir, ec.executor.root+string(os.PathSeparator))
dirPath := filepath.ToSlash(filepath.Clean(relDir))
parts := strings.Split(dirPath, "/")

// We now want to walk down into v to ensure that the only fields that
// exist at each "level" are consistent with the elements of path
var selectors []cue.Selector
for _, elem := range parts {
path := cue.MakePath(selectors...)
toCheck := v.LookupPath(path)
fieldIter, err := toCheck.Fields(cue.Definitions(true), cue.Hidden(true))
if err != nil {
ec.errorf("%v: %s: failed to create iterator over CUE value at path %v: %v", ec, absDir, path, err)
continue dirs
}
// Could be multiple bad fields at this level, report them all
var inError bool
for fieldIter.Next() {
sel := fieldIter.Selector()
if sel.LabelType() != cue.StringLabel || sel.Unquoted() != elem {
inError = true
val := fieldIter.Value()
badPath := cue.MakePath(append(selectors, sel)...)

// val.Pos() is the position of the _value_ (the RHS), not the
// field name. Hence we need to construct the format string by
// hand.
//
// TODO: work out whether we can get the location(s) of the
// label for this value in a more principled way.
pos := val.Pos()
ec.errorf("%v:%d: %v: field not allowed; expected %q", pos.Filename(), pos.Line(), badPath, elem)
}
}
if inError {
// No point descending further at this point
continue dirs
}
selectors = append(selectors, cue.Str(elem))
}
}
}

func (ec *executeContext) findPages() error {
dirsToWalk := []string{ec.executor.wd}

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/preprocessor/cmd/execute_doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func newExecuteCmd(c *Command) *cobra.Command {
cmd.Flags().Bool(string(flagNoRun), false, "whether to attempt to run scripts or not")
cmd.Flags().Bool(string(flagSkipCache), false, "skip cache checks; always run")
cmd.Flags().Bool(string(flagNoWriteCache), os.Getenv("PREPROCESSOR_NOWRITECACHE") != "", "do not write updated page cache entries. Can also be set with non-empty PREPROCESSOR_NOWRITECACHE env var.")
cmd.Flags().Bool(string(flagCheck), false, "check CUE in page roots is properly namespaced")
cmd.Flags().StringSliceVar(&hugoArgs, string(flagHugoFlag), nil, "list of flags to pass to hugo")
return cmd
}
33 changes: 33 additions & 0 deletions internal/cmd/preprocessor/cmd/testdata/execute_check.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Ensure that we get no output when --check succeeds

# Run the preprocessor
exec preprocessor execute --check
! stdout .+

-- content/dir1/page.cue --
package site

// Page value defined
content: dir1: {}
-- content/dir1/en.md --
---
title: JSON Superset
---
-- content/dir2/page.cue --
package site

// Only content defined
content: {}
-- content/dir1/en.md --
---
title: JSON Superset
---
-- content/dir3/page.cue --
package site

// No value defined
-- content/dir3/en.md --
---
title: JSON Superset
---
-- hugo/content/.gitkeep --
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Ensure that we get a sensible error message when using the
# --check flag to execute.

unquote content/dir/en.md

# Run the preprocessor
! exec preprocessor execute --check
cmpenv stderr stderr.golden

-- content/dir/page.cue --
package site

this: 5
-- content/dir/en.md --
>---
>title: JSON Superset
>---
-- hugo/content/.gitkeep --
-- stderr.golden --
** $WORK/content/dir/page.cue:3: this: field not allowed; expected "content"
terminating because of errors

0 comments on commit ed7100e

Please sign in to comment.