pkg/repo/repo.go (363 lines of code) (raw):

/* Copyright 2021 The Kubernetes Authors. 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 repo import ( "context" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/google/go-github/v33/github" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/oauth2" krgh "k8s.io/release/pkg/github" "k8s.io/test-infra/prow/git" "k8s.io/enhancements/api" "k8s.io/enhancements/pkg/kepval" "k8s.io/enhancements/pkg/yaml" ) const ( ProposalPathStub = "keps" ProposalTemplatePathStub = "NNNN-kep-template" ProposalFilename = "README.md" ProposalMetadataFilename = "kep.yaml" PRRApprovalPathStub = "prod-readiness" remoteOrg = "kubernetes" remoteRepo = "enhancements" proposalLabel = "kind/kep" ) type Repo struct { // Paths BasePath string ProposalPath string PRRApprovalPath string ProposalReadme string // Auth TokenPath string Token string // Document handlers KEPHandler *api.KEPHandler PRRHandler *api.PRRHandler // Templates ProposalTemplate []byte // Temporary caches // a local git clone of remoteOrg/remoteRepo gitRepo *git.Repo // all open pull requests for remoteOrg/remoteRepo allPRs []*github.PullRequest } // New returns a new repo client configured to use the the normal os.Stdxxx and Filesystem func New(repoPath string) (*Repo, error) { fetcher := api.DefaultGroupFetcher() return NewRepo(repoPath, fetcher) } func NewRepo(repoPath string, fetcher api.GroupFetcher) (*Repo, error) { var err error if repoPath == "" { repoPath, err = os.Getwd() if err != nil { return nil, fmt.Errorf("unable to determine enhancements repo path: %s", err) } } proposalPath := filepath.Join(repoPath, ProposalPathStub) fi, err := os.Stat(proposalPath) if err != nil { return nil, errors.Wrapf( err, "getting file info for proposal path %s", proposalPath, ) } if !fi.IsDir() { return nil, errors.Wrap( err, "checking if proposal path is a directory", ) } prrApprovalPath := filepath.Join(proposalPath, PRRApprovalPathStub) fi, err = os.Stat(prrApprovalPath) if err != nil { return nil, errors.Wrapf( err, "getting file info for PRR approval path %s", prrApprovalPath, ) } if !fi.IsDir() { return nil, errors.Wrap( err, "checking if PRR approval path is a directory", ) } proposalReadme := filepath.Join(proposalPath, "README.md") fi, err = os.Stat(proposalReadme) if err != nil { return nil, errors.Wrapf( err, "getting file info for proposal README path %s", proposalPath, ) } if !fi.Mode().IsRegular() { return nil, errors.Wrap( err, "checking if proposal README is a file", ) } groups, err := fetcher.FetchGroups() if err != nil { return nil, fmt.Errorf("fetching groups: %w", err) } prrApprovers, err := fetcher.FetchPRRApprovers() if err != nil { return nil, fmt.Errorf("fetching PRR approvers: %w", err) } kepHandler := &api.KEPHandler{Groups: groups, PRRApprovers: prrApprovers} prrHandler := &api.PRRHandler{PRRApprovers: prrApprovers} repo := &Repo{ BasePath: repoPath, ProposalPath: proposalPath, PRRApprovalPath: prrApprovalPath, ProposalReadme: proposalReadme, KEPHandler: kepHandler, PRRHandler: prrHandler, } proposalTemplate, err := repo.getProposalTemplate() if err != nil { return nil, errors.Wrap(err, "getting proposal template") } repo.ProposalTemplate = proposalTemplate // build a default client with normal os.Stdxx and Filesystem access. Tests can build their own // with appropriate test objects return repo, nil } func (r *Repo) SetGitHubToken(tokenFile string) error { if tokenFile != "" { token, err := ioutil.ReadFile(tokenFile) if err != nil { return err } r.Token = strings.Trim(string(token), "\n\r") } return nil } // getProposalTemplate reads the KEP template from the local clone of // kubernetes/enhancements. func (r *Repo) getProposalTemplate() ([]byte, error) { path := filepath.Join( r.ProposalPath, ProposalTemplatePathStub, ProposalFilename, ) return ioutil.ReadFile(path) } func (r *Repo) findLocalKEPMeta(sig string) ([]string, error) { sigPath := filepath.Join(r.ProposalPath, sig) keps := []string{} // if the SIG doesn't have a dir, it has no KEPs if _, err := os.Stat(sigPath); os.IsNotExist(err) { return keps, nil } err := filepath.Walk( sigPath, func(path string, info os.FileInfo, err error) error { logrus.Debugf("processing filename %s", info.Name()) if err != nil { return err } // true if the file is a symlink if info.Mode()&os.ModeSymlink != 0 { // Assume symlink from old KEP location to new. The new location // will be processed separately, so no need to process it here. logrus.Debugf("%s is a symlink", info.Name()) return nil } if !info.Mode().IsRegular() { return nil } if info.Name() == ProposalMetadataFilename { logrus.Debugf("adding %s as KEP metadata", info.Name()) path, err = filepath.Rel(r.BasePath, path) if err != nil { return err } keps = append(keps, path) return filepath.SkipDir } if info.Name() == ProposalFilename { return nil } return nil }, ) return keps, err } func (r *Repo) LoadLocalKEPs(sig string) ([]*api.Proposal, error) { // KEPs in the local filesystem files, err := r.findLocalKEPMeta(sig) if err != nil { return nil, errors.Wrapf( err, "searching for local KEPs from %s", sig, ) } logrus.Debugf("loading the following local KEPs: %v", files) allKEPs := make([]*api.Proposal, len(files)) for i, kepYamlPath := range files { kep, err := r.loadKEPFromYaml(r.BasePath, kepYamlPath) if err != nil { return nil, errors.Wrapf( err, "reading KEP %s from yaml", kepYamlPath, ) } allKEPs[i] = kep } logrus.Debugf("returning %d local KEPs", len(allKEPs)) return allKEPs, nil } func (r *Repo) LoadLocalKEP(sig, name string) (*api.Proposal, error) { kepPath := filepath.Join( ProposalPathStub, sig, name, ProposalMetadataFilename, ) _, err := os.Stat(kepPath) if err != nil { return nil, errors.Wrapf(err, "getting file info for %s", kepPath) } return r.loadKEPFromYaml(r.BasePath, kepPath) } func (r *Repo) LoadPullRequestKEPs(sig string) ([]*api.Proposal, error) { // Initialize github client logrus.Debugf("Initializing github client to load PRs for sig: %v", sig) var auth *http.Client ctx := context.Background() if r.Token != "" { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: r.Token}) auth = oauth2.NewClient(ctx, ts) } gh := github.NewClient(auth) // Fetch list of all PRs if none exists if r.allPRs == nil { logrus.Debugf("Initializing list of all PRs for %v/%v", remoteOrg, remoteRepo) r.allPRs = []*github.PullRequest{} opt := &github.PullRequestListOptions{ ListOptions: github.ListOptions{ PerPage: 100, }, } for { pulls, resp, err := gh.PullRequests.List( ctx, remoteOrg, remoteRepo, opt, ) if err != nil { return nil, err } r.allPRs = append(r.allPRs, pulls...) if resp.NextPage == 0 { break } opt.Page = resp.NextPage } } // Find KEP PRs for the given sig kepPRs := []*github.PullRequest{} sigLabel := strings.Replace(sig, "-", "/", 1) logrus.Debugf("Searching list of %v PRs for %v/%v with labels: [%v, %v]", len(r.allPRs), remoteOrg, remoteRepo, sigLabel, proposalLabel) for _, pr := range r.allPRs { foundKind, foundSIG := false, false for _, l := range pr.Labels { if *l.Name == proposalLabel { foundKind = true } if *l.Name == sigLabel { foundSIG = true } } if !foundKind || !foundSIG { continue } logrus.Debugf("Found #%v", pr.GetHTMLURL()) kepPRs = append(kepPRs, pr) } logrus.Debugf("Found %v PRs for %v/%v with labels: [%v, %v]", len(kepPRs), remoteOrg, remoteRepo, sigLabel, proposalLabel) if len(kepPRs) == 0 { return nil, nil } // Pull a temporary clone of the repo if none already exists if r.gitRepo == nil { g, err := git.NewClient() if err != nil { return nil, err } g.SetCredentials("", func() []byte { return []byte{} }) g.SetRemote(krgh.GitHubURL) r.gitRepo, err = g.Clone(remoteOrg, remoteRepo) if err != nil { return nil, err } } // read out each PR, and create a Proposal for each KEP that is // touched by a PR. This may result in multiple versions of the same KEP. var allKEPs []*api.Proposal for _, pr := range kepPRs { logrus.Debugf("Getting list of files for %v", pr.GetHTMLURL()) files, _, err := gh.PullRequests.ListFiles( context.Background(), remoteOrg, remoteRepo, pr.GetNumber(), &github.ListOptions{}, ) if err != nil { return nil, err } kepNames := make(map[string]bool, 10) for _, file := range files { if !strings.HasPrefix(*file.Filename, "keps/"+sig+"/") { continue } kk := strings.Split(*file.Filename, "/") if len(kk) < 3 { continue } if strings.HasSuffix(kk[2], ".md") { kepNames[kk[2][0:len(kk[2])-3]] = true } else { kepNames[kk[2]] = true } } if len(kepNames) == 0 { continue } err = r.gitRepo.CheckoutPullRequest(pr.GetNumber()) if err != nil { return nil, err } // read all these KEPs for k := range kepNames { kepPath := filepath.Join( ProposalPathStub, sig, k, ProposalMetadataFilename, ) kep, err := r.loadKEPFromYaml(r.gitRepo.Directory(), kepPath) if err != nil { logrus.Warnf("error reading KEP %v: %v", k, err) } else { kep.PRNumber = strconv.Itoa(pr.GetNumber()) allKEPs = append(allKEPs, kep) } } } return allKEPs, nil } // loadKEPFromYaml will return a Proposal from a kep.yaml at the given kepPath // within the given repoPath, or an error if the Proposal is invalid func (r *Repo) loadKEPFromYaml(repoPath, kepPath string) (*api.Proposal, error) { fullKEPPath := filepath.Join(repoPath, kepPath) b, err := ioutil.ReadFile(fullKEPPath) if err != nil { return nil, fmt.Errorf("unable to read KEP metadata for %s: %w", fullKEPPath, err) } var p api.Proposal err = yaml.UnmarshalStrict(b, &p) if err != nil { return nil, fmt.Errorf("unable to load KEP metadata: %s", err) } p.Name = filepath.Base(filepath.Dir(kepPath)) prrApprovalPath := filepath.Join(repoPath, ProposalPathStub, PRRApprovalPathStub) // Read the PRR approval file and add any listed PRR approvers in there // to the PRR approvers list in the KEP. this is a hack while we transition // away from PRR approvers listed in kep.yaml handler := r.PRRHandler err = kepval.ValidatePRR(&p, handler, prrApprovalPath) if err != nil { logrus.Errorf( "%v", errors.Wrapf(err, "validating PRR for %s", p.Name), ) } return &p, nil }