...

Source file src/edge-infra.dev/pkg/tools/hack/cmd/owners/verify.go

Documentation: edge-infra.dev/pkg/tools/hack/cmd/owners

     1  package owners
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"flag"
     7  	"fmt"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  
    14  	"slices"
    15  
    16  	"github.com/peterbourgon/ff/v3"
    17  	"github.com/peterbourgon/ff/v3/ffcli"
    18  	"gopkg.in/yaml.v2"
    19  
    20  	repoowners "edge-infra.dev/pkg/f8n/devinfra/repo/owners"
    21  	"edge-infra.dev/pkg/lib/text/drawing"
    22  )
    23  
    24  var ignore = []string{".git", "build", "tmp", ".vscode"}
    25  
    26  type verify struct {
    27  	*owners
    28  
    29  	validate bool
    30  	upToDate bool
    31  	unowned  bool
    32  }
    33  
    34  func newVerify(o *owners) *ffcli.Command {
    35  	u := &verify{owners: o}
    36  
    37  	fs := flag.NewFlagSet("hack owners verify", flag.ExitOnError)
    38  	u.owners.RegisterFlags(fs)
    39  	fs.BoolVar(&u.validate, "validate", false,
    40  		"verify that the policy-bot config is valid")
    41  	fs.BoolVar(&u.upToDate, "up-to-date", false,
    42  		"verify that the policy-bot config is up-to-date")
    43  	fs.BoolVar(&u.unowned, "unowned", false,
    44  		"gather a list of currently unowned directories")
    45  	return &ffcli.Command{
    46  		Name:    "verify",
    47  		FlagSet: fs,
    48  		Exec:    u.Exec,
    49  		Options: []ff.Option{
    50  			ff.WithEnvVarNoPrefix(),
    51  		},
    52  	}
    53  }
    54  
    55  func (v *verify) Exec(_ context.Context, _ []string) error {
    56  	if v.unowned {
    57  		_, err := gatherUnowned(".")
    58  		if err != nil {
    59  			return err
    60  		}
    61  	}
    62  
    63  	if v.upToDate {
    64  		// gather the owners files
    65  		u := &update{owners: v.owners}
    66  		data, err := u.generatePBotCfg()
    67  		if err != nil {
    68  			return err
    69  		}
    70  
    71  		// get the pbot config file
    72  		pbotData, err := v.getPolicyBotConfig()
    73  		if err != nil {
    74  			return err
    75  		}
    76  
    77  		// check if the gathered files match the config file
    78  		if !bytes.Equal(data, pbotData) {
    79  			return fmt.Errorf("pbot file is not up-to-date. try running: just update-policy-bot")
    80  		}
    81  		fmt.Println("✅ pbot file is up to date")
    82  	}
    83  
    84  	if v.validate {
    85  		return v.valid()
    86  	}
    87  	return nil
    88  }
    89  
    90  func (v *verify) valid() error {
    91  	// get the pbot config file
    92  	p := filepath.Join(v.Paths.RepoRoot, v.policyBotFile)
    93  	bytes, err := os.ReadFile(p)
    94  	if err != nil {
    95  		return fmt.Errorf("failed to read file %s: %w", v.policyBotFile, err)
    96  	}
    97  
    98  	// check if the config is valid
    99  	ok, err := isValidLocalPolicy(bytes)
   100  	if err != nil {
   101  		return fmt.Errorf("failed to validate %s: %w", v.policyBotFile, err)
   102  	}
   103  	if !ok {
   104  		return fmt.Errorf("%s is not valid", p)
   105  	}
   106  	fmt.Printf("✅ %s is a valid config", p)
   107  	return nil
   108  }
   109  
   110  func gatherUnowned(root string) ([]string, error) {
   111  	// files := map[string]bool{}
   112  	fileNames := []string{}
   113  
   114  	rootNode := &drawing.StringTree{
   115  		Data: "/",
   116  	}
   117  
   118  	nodes := map[string]*drawing.StringTree{}
   119  
   120  	err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
   121  		if err != nil {
   122  			return err
   123  		}
   124  
   125  		// check if the current dir is in the ignore list
   126  		if info.IsDir() && slices.Contains(ignore, path) {
   127  			return filepath.SkipDir
   128  		}
   129  
   130  		// if the current item isnt a dir skip it
   131  		if !info.IsDir() {
   132  			return nil
   133  		}
   134  
   135  		d, f := filepath.Split(path)
   136  		d = strings.TrimSuffix(d, "/")
   137  
   138  		if d == "" { // pkg/ cmd/
   139  			node := &drawing.StringTree{
   140  				Data: f,
   141  			}
   142  			rootNode.Children = append(rootNode.Children, node)
   143  			nodes[f] = node
   144  			return nil
   145  		}
   146  
   147  		dNode, ok := nodes[d] // cmd pkg/edge
   148  		if !ok {
   149  			dNode = &drawing.StringTree{
   150  				Data: f,
   151  			}
   152  			nodes[d] = dNode
   153  		}
   154  
   155  		pathWithOwners := filepath.Join(path, repoowners.FileName)
   156  
   157  		// if the owners file doesnt exist add it to the list
   158  		_, err = os.Stat(pathWithOwners)
   159  		if os.IsNotExist(err) {
   160  			fileNames = append(fileNames, path)
   161  			fNode, ok := nodes[path]
   162  			if !ok {
   163  				fNode = &drawing.StringTree{
   164  					Data:   f,
   165  					Labels: map[string]string{"🚫": ""},
   166  				}
   167  				nodes[path] = fNode
   168  			}
   169  			dNode.Children = append(dNode.Children, fNode)
   170  			return nil
   171  		}
   172  
   173  		data, err := os.ReadFile(pathWithOwners)
   174  		if err != nil {
   175  			return fmt.Errorf("failed to read %s: %w", pathWithOwners, err)
   176  		}
   177  
   178  		file := &repoowners.File{}
   179  		if err := yaml.UnmarshalStrict(data, file); err != nil {
   180  			return fmt.Errorf("failed to parse %s: %w", pathWithOwners, err)
   181  		}
   182  
   183  		// range over all rules
   184  		for _, rule := range file.Rules {
   185  			// range over all paths
   186  			for _, regexPath := range rule.Predicates.ChangedFiles.Paths {
   187  				// if the OWNERS files uses .* then skip the entire dir
   188  				if regexPath.String() == repoowners.Regex {
   189  					return filepath.SkipDir
   190  				}
   191  
   192  				fNode, ok := nodes[path]
   193  				if !ok {
   194  					fNode = &drawing.StringTree{
   195  						Data: f,
   196  					}
   197  					nodes[path] = fNode
   198  				}
   199  				dNode.Children = append(dNode.Children, fNode)
   200  				// todo (s185994): check if the paths listed match all files in dir
   201  			}
   202  		}
   203  
   204  		return nil
   205  	})
   206  	if err != nil {
   207  		return nil, fmt.Errorf("failed to walk filesystem: %w", err)
   208  	}
   209  
   210  	rootNode.Print()
   211  
   212  	// sort the files
   213  	sort.Slice(fileNames, func(i, j int) bool {
   214  		return fileNames[i] < fileNames[j]
   215  	})
   216  
   217  	return fileNames, nil
   218  }
   219  

View as plain text