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 }