| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "net/mail" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "time" |
| |
| "github.com/gorhill/cronexpr" |
| "github.com/maruel/subcommands" |
| "go.fuchsia.dev/infra/cmd/roller-configurator/proto" |
| ) |
| |
| func cmdValidate() *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "validate [-config <config-path>]", |
| ShortDesc: "Validate a rollers.textproto file.", |
| LongDesc: "Validate a rollers.textproto file.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &validateRun{} |
| c.Init() |
| return c |
| }, |
| } |
| } |
| |
| type validateRun struct { |
| subcommands.CommandRunBase |
| configPath string |
| } |
| |
| func (c *validateRun) Init() { |
| c.Flags.StringVar(&c.configPath, "config", "rollers.textproto", "Path to the config file to validate.") |
| } |
| |
| func (c *validateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| config, err := readConfig(c.configPath) |
| if err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| return 1 |
| } |
| // rollers.textproto files must be located in the repository root. |
| repoRoot := filepath.Dir(c.configPath) |
| if err := validate(context.Background(), repoRoot, config); err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| return 1 |
| } |
| fmt.Fprintln(a.GetOut(), "Successful validation") |
| return 0 |
| } |
| |
| func validate(ctx context.Context, repoRoot string, config *proto.Config) error { |
| var hasJiriEntities bool |
| for i, roller := range config.GetRollers() { |
| toRollDesc := roller.ProtoReflect().Descriptor().Oneofs().ByName("to_roll") |
| field := roller.ProtoReflect().WhichOneof(toRollDesc) |
| if field == nil { |
| return fmt.Errorf("entry %d is missing an entity to roll", i) |
| } |
| |
| var toValidate interface { |
| Validate(ctx context.Context, repoRoot string) error |
| } |
| switch field.Name() { |
| case "submodule": |
| toValidate = roller.GetSubmodule() |
| case "cipd_ensure_file": |
| toValidate = roller.GetCipdEnsureFile() |
| case "jiri_project": |
| toValidate = roller.GetJiriProject() |
| hasJiriEntities = true |
| case "jiri_packages": |
| toValidate = roller.GetJiriPackages() |
| hasJiriEntities = true |
| default: |
| log.Panicf("unknown to_roll type: %q", field.Name()) |
| } |
| |
| if err := toValidate.Validate(ctx, repoRoot); err != nil { |
| return err |
| } |
| |
| if schedule := roller.GetSchedule(); schedule != "" { |
| if err := validateSchedule(schedule); err != nil { |
| return err |
| } |
| } |
| |
| for _, email := range roller.GetNotifyEmails() { |
| if err := validateEmail(email); err != nil { |
| return err |
| } |
| } |
| } |
| if hasJiriEntities && config.GetDefaultCheckoutJiriManifest() == "" { |
| return fmt.Errorf("default_checkout_jiri_manifest is required to enable jiri rollers") |
| } else if !hasJiriEntities && config.GetDefaultCheckoutJiriManifest() != "" { |
| return fmt.Errorf("default_checkout_jiri_manifest need not be set") |
| } |
| return nil |
| } |
| |
| var withIntervalScheduleRE = regexp.MustCompile(`with (\d+\w+) interval`) |
| |
| func validateSchedule(schedule string) error { |
| if strings.HasPrefix(schedule, "with ") { |
| sm := withIntervalScheduleRE.FindStringSubmatch(schedule) |
| if sm == nil { |
| return fmt.Errorf("invalid schedule %q", schedule) |
| } |
| _, err := time.ParseDuration(sm[1]) |
| if err != nil { |
| return fmt.Errorf("invalid duration in schedule %q", schedule) |
| } |
| } else { |
| _, err := cronexpr.Parse(schedule) |
| if err != nil { |
| return fmt.Errorf("invalid cron schedule %q", schedule) |
| } |
| } |
| return nil |
| } |
| |
| func validateEmail(email string) error { |
| // Do a basic validity check to make sure it roughly looks like an email. |
| if _, err := mail.ParseAddress(email); err != nil { |
| return fmt.Errorf("invalid email %q", email) |
| } |
| return nil |
| } |