go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/roller-configurator/validate.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"log"
    11  	"net/mail"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/gorhill/cronexpr"
    18  	"github.com/maruel/subcommands"
    19  	"go.fuchsia.dev/infra/cmd/roller-configurator/proto"
    20  )
    21  
    22  func cmdValidate() *subcommands.Command {
    23  	return &subcommands.Command{
    24  		UsageLine: "validate [-config <config-path>]",
    25  		ShortDesc: "Validate a rollers.textproto file.",
    26  		LongDesc:  "Validate a rollers.textproto file.",
    27  		CommandRun: func() subcommands.CommandRun {
    28  			c := &validateRun{}
    29  			c.Init()
    30  			return c
    31  		},
    32  	}
    33  }
    34  
    35  type validateRun struct {
    36  	subcommands.CommandRunBase
    37  	configPath string
    38  }
    39  
    40  func (c *validateRun) Init() {
    41  	c.Flags.StringVar(&c.configPath, "config", "rollers.textproto", "Path to the config file to validate.")
    42  }
    43  
    44  func (c *validateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
    45  	config, err := readConfig(c.configPath)
    46  	if err != nil {
    47  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
    48  		return 1
    49  	}
    50  	// rollers.textproto files must be located in the repository root.
    51  	repoRoot := filepath.Dir(c.configPath)
    52  	if err := validate(context.Background(), repoRoot, config); err != nil {
    53  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
    54  		return 1
    55  	}
    56  	fmt.Fprintln(a.GetOut(), "Successful validation")
    57  	return 0
    58  }
    59  
    60  func validate(ctx context.Context, repoRoot string, config *proto.Config) error {
    61  	var hasJiriEntities bool
    62  	for i, roller := range config.GetRollers() {
    63  		toRollDesc := roller.ProtoReflect().Descriptor().Oneofs().ByName("to_roll")
    64  		field := roller.ProtoReflect().WhichOneof(toRollDesc)
    65  		if field == nil {
    66  			return fmt.Errorf("entry %d is missing an entity to roll", i)
    67  		}
    68  
    69  		var toValidate interface {
    70  			Validate(ctx context.Context, repoRoot string) error
    71  		}
    72  		switch field.Name() {
    73  		case "submodule":
    74  			toValidate = roller.GetSubmodule()
    75  		case "cipd_ensure_file":
    76  			toValidate = roller.GetCipdEnsureFile()
    77  		case "jiri_project":
    78  			toValidate = roller.GetJiriProject()
    79  			hasJiriEntities = true
    80  		case "jiri_packages":
    81  			toValidate = roller.GetJiriPackages()
    82  			hasJiriEntities = true
    83  		default:
    84  			log.Panicf("unknown to_roll type: %q", field.Name())
    85  		}
    86  
    87  		if err := toValidate.Validate(ctx, repoRoot); err != nil {
    88  			return err
    89  		}
    90  
    91  		if schedule := roller.GetSchedule(); schedule != "" {
    92  			if err := validateSchedule(schedule); err != nil {
    93  				return err
    94  			}
    95  		}
    96  
    97  		for _, email := range roller.GetNotifyEmails() {
    98  			if err := validateEmail(email); err != nil {
    99  				return err
   100  			}
   101  		}
   102  	}
   103  	if hasJiriEntities && config.GetDefaultCheckoutJiriManifest() == "" {
   104  		return fmt.Errorf("default_checkout_jiri_manifest is required to enable jiri rollers")
   105  	} else if !hasJiriEntities && config.GetDefaultCheckoutJiriManifest() != "" {
   106  		return fmt.Errorf("default_checkout_jiri_manifest need not be set")
   107  	}
   108  	return nil
   109  }
   110  
   111  var withIntervalScheduleRE = regexp.MustCompile(`with (\d+\w+) interval`)
   112  
   113  func validateSchedule(schedule string) error {
   114  	if strings.HasPrefix(schedule, "with ") {
   115  		sm := withIntervalScheduleRE.FindStringSubmatch(schedule)
   116  		if sm == nil {
   117  			return fmt.Errorf("invalid schedule %q", schedule)
   118  		}
   119  		_, err := time.ParseDuration(sm[1])
   120  		if err != nil {
   121  			return fmt.Errorf("invalid duration in schedule %q", schedule)
   122  		}
   123  	} else {
   124  		_, err := cronexpr.Parse(schedule)
   125  		if err != nil {
   126  			return fmt.Errorf("invalid cron schedule %q", schedule)
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  func validateEmail(email string) error {
   133  	// Do a basic validity check to make sure it roughly looks like an email.
   134  	if _, err := mail.ParseAddress(email); err != nil {
   135  		return fmt.Errorf("invalid email %q", email)
   136  	}
   137  	return nil
   138  }