sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/inrepoconfig.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package config 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "os" 24 "path" 25 "path/filepath" 26 "strings" 27 "time" 28 29 gitignore "github.com/denormal/go-gitignore" 30 "github.com/prometheus/client_golang/prometheus" 31 "github.com/sirupsen/logrus" 32 utilerrors "k8s.io/apimachinery/pkg/util/errors" 33 "k8s.io/apimachinery/pkg/util/sets" 34 gerritsource "sigs.k8s.io/prow/pkg/gerrit/source" 35 36 "sigs.k8s.io/prow/pkg/git/types" 37 "sigs.k8s.io/prow/pkg/git/v2" 38 "sigs.k8s.io/yaml" 39 ) 40 41 const ( 42 inRepoConfigFileName = ".prow.yaml" 43 inRepoConfigDirName = ".prow" 44 ) 45 46 var inrepoconfigRepoOpts = git.RepoOpts{ 47 // Technically we only need inRepoConfigDirName (".prow") because the 48 // default "cone mode" of sparse checkouts already include files at the 49 // toplevel (which would include ".prow.yaml"). 50 SparseCheckoutDirs: []string{inRepoConfigDirName}, 51 // The sparse checkout would avoid creating another copy of Git objects 52 // from the mirror clone into the secondary clone. 53 ShareObjectsWithPrimaryClone: true, 54 } 55 56 var inrepoconfigMetrics = struct { 57 gitCloneDuration *prometheus.HistogramVec 58 gitOtherDuration *prometheus.HistogramVec 59 }{ 60 gitCloneDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 61 Name: "inrepoconfig_git_client_acquisition_duration", 62 Help: "Seconds taken for acquiring a git client (may include an initial clone operation).", 63 Buckets: []float64{0.5, 1, 2, 5, 10, 20, 30, 45, 60, 90, 120, 180, 300, 600, 1200}, 64 }, []string{ 65 "org", 66 "repo", 67 }), 68 gitOtherDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 69 Name: "inrepoconfig_git_other_duration", 70 Help: "Seconds taken after acquiring a git client and performing all other git operations (to read the ProwYAML of the repo).", 71 Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 20, 30, 45, 60, 90, 120, 180, 300, 600}, 72 }, []string{ 73 "org", 74 "repo", 75 }), 76 } 77 78 func init() { 79 prometheus.MustRegister(inrepoconfigMetrics.gitCloneDuration) 80 prometheus.MustRegister(inrepoconfigMetrics.gitOtherDuration) 81 } 82 83 // +k8s:deepcopy-gen=true 84 85 // ProwYAML represents the content of a .prow.yaml file 86 // used to version Presubmits and Postsubmits inside the tested repo. 87 type ProwYAML struct { 88 Presets []Preset `json:"presets"` 89 Presubmits []Presubmit `json:"presubmits"` 90 Postsubmits []Postsubmit `json:"postsubmits"` 91 92 // ProwIgnored is a well known, unparsed field where non-Prow fields can 93 // be defined without conflicting with unknown field validation. 94 ProwIgnored *json.RawMessage `json:"prow_ignored,omitempty"` 95 } 96 97 // ProwYAMLGetter is used to retrieve a ProwYAML. Tests should provide 98 // their own implementation and set that on the Config. 99 type ProwYAMLGetter func(c *Config, gc git.ClientFactory, identifier, baseBranch, baseSHA string, headSHAs ...string) (*ProwYAML, error) 100 101 // Verify prowYAMLGetterWithDefaults and prowYAMLGetter are both of type 102 // ProwYAMLGetter. 103 var _ ProwYAMLGetter = prowYAMLGetterWithDefaults 104 var _ ProwYAMLGetter = prowYAMLGetter 105 106 // InRepoConfigGetter defines a common interface that both the Moonraker client 107 // and raw InRepoConfigCache can implement. This way, Prow components like Sub 108 // and Gerrit can choose either one (based on runtime flags), but regardless of 109 // the choice the surrounding code can still just call this GetProwYAML() 110 // interface method (without being aware whether the underlying implementation 111 // is going over the network to Moonraker or is done locally with the local 112 // InRepoConfigCache (LRU cache)). 113 type InRepoConfigGetter interface { 114 GetInRepoConfig(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) 115 GetPresubmits(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) ([]Presubmit, error) 116 GetPostsubmits(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) ([]Postsubmit, error) 117 } 118 119 // prowYAMLGetter is like prowYAMLGetterWithDefaults, but without default values 120 // (it does not call DefaultAndValidateProwYAML()). Its sole purpose is to allow 121 // caching of ProwYAMLs that are retrieved purely from the inrepoconfig's repo, 122 // __without__ having the contents modified by the main Config's own settings 123 // (which happens mostly inside DefaultAndValidateProwYAML()). prowYAMLGetter is 124 // only used by cache.GetPresubmits() and cache.GetPostsubmits(). 125 func prowYAMLGetter( 126 c *Config, 127 gc git.ClientFactory, 128 identifier string, 129 baseBranch string, 130 baseSHA string, 131 headSHAs ...string) (*ProwYAML, error) { 132 133 log := logrus.WithField("repo", identifier) 134 135 if gc == nil { 136 log.Error("prowYAMLGetter was called with a nil git client") 137 return nil, errors.New("gitClient is nil") 138 } 139 140 orgRepo := *NewOrgRepo(identifier) 141 if orgRepo.Repo == "" { 142 return nil, fmt.Errorf("didn't get two results when splitting repo identifier %q", identifier) 143 } 144 145 timeBeforeClone := time.Now() 146 repoOpts := inrepoconfigRepoOpts 147 // For Gerrit, the baseSHA could appear as a headSHA for postsubmits if the 148 // change was a fast-forward merge. So we need to dedupe it with sets. 149 repoOpts.NeededCommits = sets.New(baseSHA) 150 repoOpts.NeededCommits.Insert(headSHAs...) 151 if baseBranch != "" { 152 repoOpts.BranchesToRetarget = map[string]string{baseBranch: baseSHA} 153 } 154 repo, err := gc.ClientForWithRepoOpts(orgRepo.Org, orgRepo.Repo, repoOpts) 155 inrepoconfigMetrics.gitCloneDuration.WithLabelValues(orgRepo.Org, orgRepo.Repo).Observe((float64(time.Since(timeBeforeClone).Seconds()))) 156 if err != nil { 157 return nil, fmt.Errorf("failed to clone repo for %q: %w", identifier, err) 158 } 159 timeAfterClone := time.Now() 160 defer func() { 161 if err := repo.Clean(); err != nil { 162 log.WithError(err).Error("Failed to clean up repo.") 163 } 164 inrepoconfigMetrics.gitOtherDuration.WithLabelValues(orgRepo.Org, orgRepo.Repo).Observe((float64(time.Since(timeAfterClone).Seconds()))) 165 }() 166 167 if err := repo.Config("user.name", "prow"); err != nil { 168 return nil, err 169 } 170 if err := repo.Config("user.email", "prow@localhost"); err != nil { 171 return nil, err 172 } 173 if err := repo.Config("commit.gpgsign", "false"); err != nil { 174 return nil, err 175 } 176 177 // TODO(mpherman): This is to hopefully mittigate issue with gerrit merges. Need to come up with a solution that checks 178 // each CLs merge strategy as they can differ. ifNecessary is just the gerrit default 179 var mergeMethod types.PullRequestMergeType 180 if gerritsource.IsGerritOrg(identifier) { 181 mergeMethod = types.MergeIfNecessary 182 } else { 183 mergeMethod = c.Tide.MergeMethod(orgRepo) 184 } 185 186 log.WithField("merge-strategy", mergeMethod).Debug("Using merge strategy.") 187 if err := repo.MergeAndCheckout(baseSHA, string(mergeMethod), headSHAs...); err != nil { 188 return nil, fmt.Errorf("failed to merge: %w", err) 189 } 190 191 return ReadProwYAML(log, repo.Directory(), false) 192 } 193 194 // ReadProwYAML parses the .prow.yaml file or .prow directory, no commit checkout or defaulting is included. 195 func ReadProwYAML(log *logrus.Entry, dir string, strict bool) (*ProwYAML, error) { 196 prowYAML := &ProwYAML{} 197 var opts []yaml.JSONOpt 198 if strict { 199 opts = append(opts, yaml.DisallowUnknownFields) 200 } 201 202 prowYAMLDirPath := path.Join(dir, inRepoConfigDirName) 203 log.Debugf("Attempting to read config files under %q.", prowYAMLDirPath) 204 if fileInfo, err := os.Stat(prowYAMLDirPath); !os.IsNotExist(err) && err == nil && fileInfo.IsDir() { 205 mergeProwYAML := func(a, b *ProwYAML) *ProwYAML { 206 c := &ProwYAML{} 207 c.Presets = append(a.Presets, b.Presets...) 208 c.Presubmits = append(a.Presubmits, b.Presubmits...) 209 c.Postsubmits = append(a.Postsubmits, b.Postsubmits...) 210 211 return c 212 } 213 prowIgnore, err := gitignore.NewRepositoryWithFile(dir, ProwIgnoreFileName) 214 if err != nil { 215 return nil, fmt.Errorf("failed to create `%s` parser: %w", ProwIgnoreFileName, err) 216 } 217 err = filepath.Walk(prowYAMLDirPath, func(p string, info os.FileInfo, err error) error { 218 if err != nil { 219 return err 220 } 221 if !info.IsDir() && (filepath.Ext(p) == ".yaml" || filepath.Ext(p) == ".yml") { 222 // Use 'Match' directly because 'Ignore' and 'Include' don't work properly for repositories. 223 match := prowIgnore.Match(p) 224 if match != nil && match.Ignore() { 225 return nil 226 } 227 log.Debugf("Reading YAML file %q", p) 228 bytes, err := os.ReadFile(p) 229 if err != nil { 230 return err 231 } 232 partialProwYAML := &ProwYAML{} 233 if err := yaml.Unmarshal(bytes, partialProwYAML, opts...); err != nil { 234 return fmt.Errorf("failed to unmarshal %q: %w", p, err) 235 } 236 prowYAML = mergeProwYAML(prowYAML, partialProwYAML) 237 } 238 return err 239 }) 240 if err != nil { 241 return nil, fmt.Errorf("failed to read contents of directory %q: %w", inRepoConfigDirName, err) 242 } 243 } else { 244 if !os.IsNotExist(err) { 245 return nil, fmt.Errorf("reading %q: %w", prowYAMLDirPath, err) 246 } 247 log.WithField("file", inRepoConfigFileName).Debug("Attempting to get inreconfigfile") 248 prowYAMLFilePath := path.Join(dir, inRepoConfigFileName) 249 if _, err := os.Stat(prowYAMLFilePath); err == nil { 250 bytes, err := os.ReadFile(prowYAMLFilePath) 251 if err != nil { 252 return nil, fmt.Errorf("failed to read %q: %w", prowYAMLDirPath, err) 253 } 254 if err := yaml.Unmarshal(bytes, prowYAML, opts...); err != nil { 255 return nil, fmt.Errorf("failed to unmarshal %q: %w", prowYAMLDirPath, err) 256 } 257 } else { 258 if !os.IsNotExist(err) { 259 return nil, fmt.Errorf("failed to check if file %q exists: %w", prowYAMLDirPath, err) 260 } 261 } 262 } 263 return prowYAML, nil 264 } 265 266 // prowYAMLGetterWithDefaults is like prowYAMLGetter, but additionally sets 267 // defaults by calling DefaultAndValidateProwYAML. 268 func prowYAMLGetterWithDefaults( 269 c *Config, 270 gc git.ClientFactory, 271 identifier string, 272 baseBranch string, 273 baseSHA string, 274 headSHAs ...string) (*ProwYAML, error) { 275 276 prowYAML, err := prowYAMLGetter(c, gc, identifier, baseBranch, baseSHA, headSHAs...) 277 if err != nil { 278 return nil, err 279 } 280 281 // Mutate prowYAML to default values as necessary. 282 if err := DefaultAndValidateProwYAML(c, prowYAML, identifier); err != nil { 283 return nil, err 284 } 285 286 return prowYAML, nil 287 } 288 289 func DefaultAndValidateProwYAML(c *Config, p *ProwYAML, identifier string) error { 290 if err := defaultPresubmits(p.Presubmits, p.Presets, c, identifier); err != nil { 291 return err 292 } 293 if err := defaultPostsubmits(p.Postsubmits, p.Presets, c, identifier); err != nil { 294 return err 295 } 296 if err := c.validatePresubmits(append(p.Presubmits, c.GetPresubmitsStatic(identifier)...)); err != nil { 297 return err 298 } 299 if err := c.validatePostsubmits(append(p.Postsubmits, c.GetPostsubmitsStatic(identifier)...)); err != nil { 300 return err 301 } 302 303 var errs []error 304 for _, pre := range p.Presubmits { 305 if !c.InRepoConfigAllowsCluster(pre.Cluster, identifier) { 306 errs = append(errs, fmt.Errorf("cluster %q is not allowed for repository %q", pre.Cluster, identifier)) 307 } 308 } 309 for _, post := range p.Postsubmits { 310 if !c.InRepoConfigAllowsCluster(post.Cluster, identifier) { 311 errs = append(errs, fmt.Errorf("cluster %q is not allowed for repository %q", post.Cluster, identifier)) 312 } 313 } 314 315 if len(errs) == 0 { 316 log := logrus.WithField("repo", identifier) 317 log.Debugf("Successfully got %d presubmits and %d postsubmits.", len(p.Presubmits), len(p.Postsubmits)) 318 } 319 320 return utilerrors.NewAggregate(errs) 321 } 322 323 // ContainsInRepoConfigPath indicates whether the specified list of changed 324 // files (repo relative paths) includes a file that might be an inrepo config file. 325 // 326 // This function could report a false positive as it doesn't consider .prowignore files. 327 // It is designed to be used to help short circuit when we know a change doesn't touch 328 // the inrepo config. 329 func ContainsInRepoConfigPath(files []string) bool { 330 for _, file := range files { 331 if file == inRepoConfigFileName { 332 return true 333 } 334 if strings.HasPrefix(file, inRepoConfigDirName+"/") { 335 return true 336 } 337 } 338 return false 339 }