blob: ed6d0831afb994908ecbd05004e51d9627774b5d [file] [log] [blame]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
var (
validateCmd *flag.FlagSet
owlBotConfigPath string
processorConfigPath string
validateGoogleapisDir string
lastSegmentExecptions = map[string]bool{
"admin": true, // firestore
"common": true, // oslogin
"type": true, // shopping et al.
"autogen": true, // longrunning
"longrunning": true,
}
moduleAPIVersionRegex = regexp.MustCompile(`\/api(v[1-9]+[a-z0-9]*)`)
directoryAPIVersionRegex = regexp.MustCompile(`\/(v[1-9]+[a-z0-9]*)`)
)
const (
defaultOwlBotConfig = ".github/.OwlBot.yaml"
defaultProcessorConfig = "internal/postprocessor/config.yaml"
)
func init() {
validateCmd = flag.NewFlagSet("validate", flag.ExitOnError)
validateCmd.StringVar(&owlBotConfigPath, "owl-bot-config", "", "Absolute path to OwlBot config. Defaults to $PWD/"+defaultOwlBotConfig)
validateCmd.StringVar(&processorConfigPath, "processor-config", "", "Absolute path to PostProcessor config. Defaults to $PWD/"+defaultProcessorConfig)
validateCmd.StringVar(&validateGoogleapisDir, "googleapis-dir", "", "Absolute path to googleapis directory - enables file existence check(s). Default disabled.")
}
func validate() error {
validateCmd.Parse(os.Args[2:])
dir, err := os.Getwd()
if err != nil {
return err
}
if owlBotConfigPath == "" {
owlBotConfigPath = filepath.Join(dir, defaultOwlBotConfig)
}
if processorConfigPath == "" {
processorConfigPath = filepath.Join(dir, defaultProcessorConfig)
}
log.Println("owl-bot-config set to", owlBotConfigPath)
log.Println("processor-config set to", processorConfigPath)
log.Println("googleapis-dir set to", validateGoogleapisDir)
ppc, obc, err := loadConfigs(processorConfigPath, owlBotConfigPath)
if err != nil {
return err
}
if err := validatePostProcessorConfig(ppc); err != nil {
log.Println("error validating post processor config")
return err
}
if err := validateOwlBotConfig(obc, ppc); err != nil {
log.Println("error validating OwlBot config")
return err
}
return nil
}
func validatePostProcessorConfig(ppc *postProcessorConfig) error {
// Verify no duplicate module entries - `modules` property in YAML.
mods := make(map[string]bool, len(ppc.Modules))
for _, m := range ppc.Modules {
if seen := mods[m]; seen {
return fmt.Errorf("duplicate post-processor modules entry: %s", m)
}
mods[m] = true
}
serviceConfigs := make(map[string]*serviceConfigEntry)
for _, s := range ppc.ServiceConfigs {
if strings.Contains(s.InputDirectory, "grafeas") {
// Skip grafeas because it's an oddity that won't change anytime soon.
continue
}
// Verify no duplicate service config entries by `import-path`.
if _, seen := serviceConfigs[s.ImportPath]; seen {
return fmt.Errorf("duplicate post-processor service-configs entry for import-path: %s", s.ImportPath)
}
if err := validateServiceConfigEntry(s); err != nil {
return err
}
serviceConfigs[s.ImportPath] = s
}
return nil
}
func validateServiceConfigEntry(s *serviceConfigEntry) error {
if !strings.HasPrefix(s.ImportPath, "cloud.google.com/go/") {
return fmt.Errorf("import-path should start with 'cloud.google.com/go/': %s", s.ImportPath)
}
// Verify that import-path ends with "apiv" suffix.
importMatches := moduleAPIVersionRegex.FindAllStringSubmatch(s.ImportPath, 1)
last := s.ImportPath[strings.LastIndex(s.ImportPath, "/")+1:]
if len(importMatches) == 0 && !lastSegmentExecptions[last] {
return fmt.Errorf("import-path should have an api version in format 'apiv[a-b1-9]+': %s", s.ImportPath)
}
// Verify that input-directory ends with version suffix.
dirMatches := directoryAPIVersionRegex.FindAllStringSubmatch(s.InputDirectory, -1)
last = s.InputDirectory[strings.LastIndex(s.InputDirectory, "/")+1:]
if len(dirMatches) == 0 && !lastSegmentExecptions[last] {
return fmt.Errorf("import-path should have an api version in format 'v[a-b1-9]+': %s", s.InputDirectory)
}
// Verify import-path version matches api version in input-directory.
// Skip this if there were no matches for the expected segments.
if len(dirMatches) > 0 && len(importMatches) > 0 {
importVersion := importMatches[0][1]
dirVersion := dirMatches[0][1]
if importVersion != dirVersion {
return fmt.Errorf("mismatched input-directory (%s) and import-path (%s) versions: %s vs. %s", s.InputDirectory, s.ImportPath, dirVersion, importVersion)
}
}
// Verify that the service-config file actually exists, if requested.
if validateGoogleapisDir != "" {
serviceConfigPath := filepath.Join(validateGoogleapisDir, s.InputDirectory, s.ServiceConfig)
if _, err := os.Stat(serviceConfigPath); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("service-config file does not exist: %s", serviceConfigPath)
}
}
return nil
}
func validateOwlBotConfig(obc *owlBotConfig, ppc *postProcessorConfig) error {
// Collect all API directories with post processor configs to ensure each
// has an appropriate OwlBot config.
postProcessedDirectories := make(map[string]*serviceConfigEntry, len(ppc.ServiceConfigs))
postProcessedImportPaths := make(map[string]*serviceConfigEntry, len(ppc.ServiceConfigs))
for _, s := range ppc.ServiceConfigs {
postProcessedDirectories[s.InputDirectory] = s
importPath := s.ImportPath
if s.RelPath != "" {
importPath = filepath.Join("cloud.google.com/go", s.RelPath)
}
postProcessedImportPaths[importPath] = s
}
sources := make(map[string]bool, len(obc.DeepCopyRegex))
for _, dcr := range obc.DeepCopyRegex {
// Verify no duplicate DeepCopyRegex configs
if sources[dcr.Source] {
return fmt.Errorf("duplicate deep-copy-regex entry: %s", dcr.Source)
}
// Verify that each DeepCopyRegex has a corresponding PostProcessor config
// entry. Also detects if there is typo in the DeepCopyRegex source.
//
// Substring from 1 to trim the leading '/' from Source.
apiSource := dcr.Source[1:strings.Index(dcr.Source, "/cloud.google.com")]
if _, ok := postProcessedDirectories[apiSource]; !ok {
return fmt.Errorf("copied directory is missing a post-processor config or vice versa: %s", dcr.Source)
}
sources[dcr.Source] = true
}
removals := make(map[string]bool, len(obc.DeepRemoveRegex))
for _, drr := range obc.DeepRemoveRegex {
drr = strings.TrimSuffix(drr, "/")
// Verify no duplicate deep-remove-regex entries.
if removals[drr] {
return fmt.Errorf("duplicate deep-remove-regex entry: %s", drr)
}
// Verify deep-remove-regex is associated with a PostProcessor config entry.
// Also detects if there is typo in the deep-remove-regex entry.
if !strings.HasPrefix(drr, "/internal/generated/snippets") {
fullImportPath := filepath.Join("cloud.google.com/go", drr)
if _, ok := postProcessedImportPaths[fullImportPath]; !ok {
return fmt.Errorf("removed importpath is missing a post-processor config or vice versa: %s", drr)
}
}
removals[drr] = true
}
return nil
}