blob: 982c27b2f808d53f3cc762895658d0f72caa8f48 [file] [log] [blame]
// 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
}