blob: ea4fd6c672afe64c7228e32f4635ab1a2db544e6 [file] [log] [blame]
package main
/*
Read and validate an AutoRoll config file.
*/
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"path/filepath"
"github.com/flynn/json5"
"github.com/pmezard/go-difflib/difflib"
"go.skia.org/infra/autoroll/go/roller"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/task_driver/go/lib/os_steps"
"go.skia.org/infra/task_driver/go/td"
)
var (
// Required properties for this task.
projectId = flag.String("project_id", "", "ID of the Google Cloud project.")
taskId = flag.String("task_id", "", "ID of this task.")
taskName = flag.String("task_name", "", "Name of the task.")
workdir = flag.String("workdir", ".", "Working directory")
config = flag.String("config", "", "Config file or dir of config files to validate.")
// Optional flags.
local = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
)
func validateConfig(ctx context.Context, f string) (string, error) {
var rollerName string
return rollerName, td.Do(ctx, td.Props(fmt.Sprintf("Validate %s", f)), func(ctx context.Context) error {
// Decode the config.
var cfg roller.AutoRollerConfig
if err := util.WithReadFile(f, func(r io.Reader) error {
// TODO(borenet): This will just ignore any extraneous keys!
return json5.NewDecoder(r).Decode(&cfg)
}); err != nil {
return fmt.Errorf("Failed to read %s: %s", f, err)
}
// Re-encode the config back to JSON. Note that the encoder respects
// struct field ordering, whereas it sorts map keys; in order to obtain
// a comparable JSON encoding, we'll have to decode it again into a map,
// then encode yet again.
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
return skerr.Wrap(err)
}
var m map[string]interface{}
if err := json.NewDecoder(&buf).Decode(&m); err != nil {
return skerr.Wrap(err)
}
expectedBytes, err := json.MarshalIndent(m, "", " ")
if err != nil {
return skerr.Wrap(err)
}
expected := string(expectedBytes)
// Read the original config file into a flat map[string]interface{},
// then re-encode it as JSON to strip the comments.
m = nil
if err := util.WithReadFile(f, func(r io.Reader) error {
return json5.NewDecoder(r).Decode(&m)
}); err != nil {
return skerr.Wrap(err)
}
actualBytes, err := json.MarshalIndent(m, "", " ")
if err != nil {
return skerr.Wrap(err)
}
actual := string(actualBytes)
if actual != expected {
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(expected),
B: difflib.SplitLines(actual),
FromFile: "Expected",
ToFile: "Actual",
Context: 3,
Eol: "\n",
})
if err != nil {
return skerr.Wrap(err)
}
return skerr.Fmt("Config file %q contains unused keys:\n%s", f, diff)
}
// Validate the config. We do this after the above checks, because
// Validate() may propagate some shared configuration entries downward,
// and that would cause the above comparison to fail.
if err := cfg.Validate(); err != nil {
return fmt.Errorf("%s failed validation: %s", f, err)
}
rollerName = cfg.RollerName
return nil
})
}
func main() {
// Setup.
ctx := td.StartRun(projectId, taskId, taskName, output, local)
defer td.EndRun(ctx)
if *config == "" {
td.Fatalf(ctx, "--config is required.")
}
// Gather files to validate.
configFiles := []string{}
f, err := os_steps.Stat(ctx, *config)
if err != nil {
td.Fatal(ctx, err)
}
if f.IsDir() {
files, err := os_steps.ReadDir(ctx, *config)
if err != nil {
td.Fatal(ctx, err)
}
for _, f := range files {
// Ignore subdirectories and file names starting with '.'
if !f.IsDir() && f.Name()[0] != '.' {
configFiles = append(configFiles, filepath.Join(*config, f.Name()))
}
}
} else {
configFiles = append(configFiles, *config)
}
// Validate the file(s).
rollers := make(map[string]string, len(configFiles))
for _, f := range configFiles {
rollerName, err := validateConfig(ctx, f)
if err != nil {
td.Fatal(ctx, err)
}
if otherFile, ok := rollers[rollerName]; ok {
td.Fatalf(ctx, "Roller %q is defined in both %s and %s", rollerName, f, otherFile)
}
rollers[rollerName] = f
}
sklog.Infof("Validated %d files.", len(configFiles))
}