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
This commit is contained in:
Adin Scannell
2021-01-05 10:43:04 -08:00
committed by gVisor bot
parent 622db84e4b
commit 2a5d3c248f
12 changed files with 345 additions and 6 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 * * *'
+1 -1
View File
@@ -1,6 +1,6 @@
# Labeler labels incoming pull requests.
name: "Labeler"
on:
"on":
- pull_request
jobs:
+1 -1
View File
@@ -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 * * *"
+19
View File
@@ -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",
+35 -1
View File
@@ -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",
)
+2
View File
@@ -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(),
+97
View File
@@ -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
}
+13
View File
@@ -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",
],
)
+41
View File
@@ -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,
)
+133
View File
@@ -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)
}
}