diff options
author | Adin Scannell <ascannell@google.com> | 2021-01-05 10:40:58 -0800 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2021-01-05 10:43:04 -0800 |
commit | 2a5d3c248fbccfd1f711d027d70ba855625f22f3 (patch) | |
tree | 62c5dc39a60a1cfa90cafe7bcd5072576f2011cf | |
parent | 622db84e4bba468205c85c80a93b8b9a9c2c9ee3 (diff) |
Add YAML validation for configuration files.
For validation, the "on" key in existing YAML files is changed to a literal
string. In the YAML spec, on is a keyword which encodes a boolean value, so
without relying on a specific implementation the YAML files are technically
not encoding an object that complies with the specification.
PiperOrigin-RevId: 350172147
-rw-r--r-- | .github/workflows/build.yml | 2 | ||||
-rw-r--r-- | .github/workflows/go.yml | 2 | ||||
-rw-r--r-- | .github/workflows/issue_reviver.yml | 2 | ||||
-rw-r--r-- | .github/workflows/labeler.yml | 2 | ||||
-rw-r--r-- | .github/workflows/stale.yml | 2 | ||||
-rw-r--r-- | BUILD | 19 | ||||
-rw-r--r-- | WORKSPACE | 36 | ||||
-rw-r--r-- | tools/nogo/BUILD | 2 | ||||
-rw-r--r-- | tools/nogo/config-schema.json | 97 | ||||
-rw-r--r-- | tools/yamltest/BUILD | 13 | ||||
-rw-r--r-- | tools/yamltest/defs.bzl | 41 | ||||
-rw-r--r-- | tools/yamltest/main.go | 133 |
12 files changed, 345 insertions, 6 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3be10b9bb..ab0bf9cb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ # This workflow also generates the build badge that is referred to by # the main README. name: "Build" -on: +"on": push: branches: - master diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ff3059e2a..e62991691 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -3,7 +3,7 @@ # workflow simply generates and pushes the branch, as long as appropriate # permissions are available. name: "Go" -on: +"on": push: branches: - master diff --git a/.github/workflows/issue_reviver.yml b/.github/workflows/issue_reviver.yml index f03b814c9..3bd883035 100644 --- a/.github/workflows/issue_reviver.yml +++ b/.github/workflows/issue_reviver.yml @@ -1,7 +1,7 @@ # This workflow revives issues that are still referenced in the code, and may # have been accidentally closed or marked stale. name: "Issue reviver" -on: +"on": schedule: - cron: '0 0 * * *' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index a53fdb3e9..3a19065e1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,6 @@ # Labeler labels incoming pull requests. name: "Labeler" -on: +"on": - pull_request jobs: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index be10c5bc4..3a4aa22e2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,7 @@ # The stale workflow closes stale issues and pull requests, unless specific # tags have been applied in order to keep them open. name: "Close stale issues" -on: +"on": schedule: - cron: "0 0 * * *" @@ -1,5 +1,6 @@ load("//tools:defs.bzl", "build_test", "gazelle", "go_path") load("//tools/nogo:defs.bzl", "nogo_config") +load("//tools/yamltest:defs.bzl", "yaml_test") load("//website:defs.bzl", "doc") package(licenses = ["notice"]) @@ -50,6 +51,24 @@ doc( weight = "99", ) +yaml_test( + name = "nogo_config_test", + srcs = glob(["nogo*.yaml"]), + schema = "//tools/nogo:config-schema.json", +) + +yaml_test( + name = "github_workflows_test", + srcs = glob([".github/workflows/*.yml"]), + schema = "@github_workflow_schema//file", +) + +yaml_test( + name = "buildkite_pipelines_test", + srcs = glob([".buildkite/*.yaml"]), + schema = "@buildkite_pipeline_schema//file", +) + # The sandbox filegroup is used for sandbox-internal dependencies. package_group( name = "sandbox", @@ -1,4 +1,4 @@ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") # Bazel/starlark utilities. @@ -176,6 +176,19 @@ http_archive( ], ) +# Schemas for testing. +http_file( + name = "buildkite_pipeline_schema", + sha256 = "3369c58038b4d55c08928affafb653716eb1e7b3cabb4a391aef979dd921f4e1", + urls = ["https://raw.githubusercontent.com/buildkite/pipeline-schema/f7a0894074d194bcf19eec5411fec0528f7f4180/schema.json"], +) + +http_file( + name = "github_workflow_schema", + sha256 = "2c375bb43dbc8b32b1bed46c290d0b70a8fa2aca7a5484dfca1b6e9c38cf9e7a", + urls = ["https://raw.githubusercontent.com/SchemaStore/schemastore/27612065234778feaac216ce14dd47846fe0a2dd/src/schemas/json/github-workflow.json"], +) + # External Go repositories. # # Unfortunately, gazelle will automatically parse go modules in the @@ -1391,3 +1404,24 @@ go_repository( sum = "h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=", version = "v0.0.0-20190801114015-581e00157fb1", ) + +go_repository( + name = "com_github_xeipuuv_gojsonpointer", + importpath = "github.com/xeipuuv/gojsonpointer", + sum = "h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=", + version = "v0.0.0-20190905194746-02993c407bfb", +) + +go_repository( + name = "com_github_xeipuuv_gojsonreference", + importpath = "github.com/xeipuuv/gojsonreference", + sum = "h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=", + version = "v0.0.0-20180127040603-bd5ef7bd5415", +) + +go_repository( + name = "com_github_xeipuuv_gojsonschema", + importpath = "github.com/xeipuuv/gojsonschema", + sum = "h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=", + version = "v1.2.0", +) diff --git a/tools/nogo/BUILD b/tools/nogo/BUILD index 12b8b597c..566e0889e 100644 --- a/tools/nogo/BUILD +++ b/tools/nogo/BUILD @@ -3,6 +3,8 @@ load("//tools/nogo:defs.bzl", "nogo_objdump_tool", "nogo_stdlib", "nogo_target") package(licenses = ["notice"]) +exports_files(["config-schema.json"]) + nogo_target( name = "target", goarch = select_goarch(), diff --git a/tools/nogo/config-schema.json b/tools/nogo/config-schema.json new file mode 100644 index 000000000..3c25fe221 --- /dev/null +++ b/tools/nogo/config-schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "definitions": { + "group": { + "type": "object", + "properties": { + "name": { + "description": "The name of the group.", + "type": "string" + }, + "regex": { + "description": "A regular expression for matching paths.", + "type": "string" + }, + "default": { + "description": "Whether the group is enabled by default.", + "type": "boolean" + } + }, + "required": [ + "name", + "regex", + "default" + ], + "additionalProperties": false + }, + "regexlist": { + "description": "A list of regular expressions.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "rule": { + "type": "object", + "properties": { + "exclude": { + "description": "A regular expression for paths to exclude.", + "$ref": "#/definitions/regexlist" + }, + "suppress": { + "description": "A regular expression for messages to suppress.", + "$ref": "#/definitions/regexlist" + } + }, + "additionalProperties": false + }, + "ruleList": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/rule" + }, + { + "type": "null" + } + ] + } + } + }, + "properties": { + "groups": { + "description": "A definition of all groups.", + "type": "array", + "items": { + "$ref": "#/definitions/group" + }, + "minItems": 1 + }, + "global": { + "description": "A global set of rules.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/rule" + } + }, + "analyzers": { + "description": "A definition of all groups.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ruleList" + } + } + }, + "required": [ + "groups" + ], + "additionalProperties": false +} diff --git a/tools/yamltest/BUILD b/tools/yamltest/BUILD new file mode 100644 index 000000000..475b3badd --- /dev/null +++ b/tools/yamltest/BUILD @@ -0,0 +1,13 @@ +load("//tools:defs.bzl", "go_binary") + +package(licenses = ["notice"]) + +go_binary( + name = "yamltest", + srcs = ["main.go"], + visibility = ["//visibility:public"], + deps = [ + "@com_github_xeipuuv_gojsonschema//:go_default_library", + "@in_gopkg_yaml_v2//:go_default_library", + ], +) diff --git a/tools/yamltest/defs.bzl b/tools/yamltest/defs.bzl new file mode 100644 index 000000000..fd04f947d --- /dev/null +++ b/tools/yamltest/defs.bzl @@ -0,0 +1,41 @@ +"""Tools for testing yaml files against schemas.""" + +def _yaml_test_impl(ctx): + """Implementation for yaml_test.""" + runner = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write(runner, "\n".join([ + "#!/bin/bash", + "set -euo pipefail", + "%s -schema=%s -- %s" % ( + ctx.files._tool[0].short_path, + ctx.files.schema[0].short_path, + " ".join([f.short_path for f in ctx.files.srcs]), + ), + ]), is_executable = True) + return [DefaultInfo( + runfiles = ctx.runfiles(files = ctx.files._tool + ctx.files.schema + ctx.files.srcs), + executable = runner, + )] + +yaml_test = rule( + implementation = _yaml_test_impl, + doc = "Tests a yaml file against a schema.", + attrs = { + "srcs": attr.label_list( + doc = "The input yaml files.", + mandatory = True, + allow_files = True, + ), + "schema": attr.label( + doc = "The schema file in JSON schema format.", + allow_single_file = True, + mandatory = True, + ), + "_tool": attr.label( + executable = True, + cfg = "host", + default = Label("//tools/yamltest:yamltest"), + ), + }, + test = True, +) diff --git a/tools/yamltest/main.go b/tools/yamltest/main.go new file mode 100644 index 000000000..88271fb66 --- /dev/null +++ b/tools/yamltest/main.go @@ -0,0 +1,133 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Binary yamltest does strict yaml parsing and validation. +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "os" + + "github.com/xeipuuv/gojsonschema" + yaml "gopkg.in/yaml.v2" +) + +func fixup(v interface{}) (interface{}, error) { + switch x := v.(type) { + case map[interface{}]interface{}: + // Coerse into a string-based map, required for yaml. + strMap := make(map[string]interface{}) + for k, v := range x { + strK, ok := k.(string) + if !ok { + // This cannot be converted to JSON at all. + return nil, fmt.Errorf("invalid key %T in (%#v)", k, x) + } + fv, err := fixup(v) + if err != nil { + return nil, fmt.Errorf(".%s%w", strK, err) + } + strMap[strK] = fv + } + return strMap, nil + case []interface{}: + for i := range x { + fv, err := fixup(x[i]) + if err != nil { + return nil, fmt.Errorf("[%d]%w", i, err) + } + x[i] = fv + } + return x, nil + default: + return v, nil + } +} + +func loadFile(filename string) (gojsonschema.JSONLoader, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + dec := yaml.NewDecoder(f) + dec.SetStrict(true) + var object interface{} + if err := dec.Decode(&object); err != nil { + return nil, err + } + fixedObject, err := fixup(object) // For serialization. + if err != nil { + return nil, err + } + bytes, err := json.Marshal(fixedObject) + if err != nil { + return nil, err + } + return gojsonschema.NewStringLoader(string(bytes)), nil +} + +var schema = flag.String("schema", "", "path to JSON schema file.") + +func main() { + flag.Parse() + if *schema == "" || len(flag.Args()) == 0 { + flag.Usage() + os.Exit(2) + } + + // Construct our schema loader. + schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", *schema)) + + // Parse all documents. + allErrors := make(map[string][]error) + for _, filename := range flag.Args() { + // Record the filename with an empty slice for below, where + // we will emit all files (even those without any errors). + allErrors[filename] = nil + documentLoader, err := loadFile(filename) + if err != nil { + allErrors[filename] = append(allErrors[filename], err) + continue + } + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + allErrors[filename] = append(allErrors[filename], err) + continue + } + for _, desc := range result.Errors() { + allErrors[filename] = append(allErrors[filename], errors.New(desc.String())) + } + } + + // Print errors in yaml format. + totalErrors := 0 + for filename, errs := range allErrors { + totalErrors += len(errs) + if len(errs) == 0 { + fmt.Fprintf(os.Stderr, "%s: ✓\n", filename) + continue + } + fmt.Fprintf(os.Stderr, "%s:\n", filename) + for _, err := range errs { + fmt.Fprintf(os.Stderr, "- %s\n", err) + } + } + if totalErrors != 0 { + os.Exit(1) + } +} |