Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/pj-rehearse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type options struct {
githubEventServerOptions githubeventserver.Options
github prowflagutil.GitHubOptions
config configflagutil.ConfigOptions
rehearsalTagConfigFile string
}

func gatherOptions() (options, error) {
Expand Down Expand Up @@ -86,6 +87,7 @@ func gatherOptions() (options, error) {
o.github.AddFlags(fs)
o.githubEventServerOptions.Bind(fs)
o.config.AddFlags(fs)
fs.StringVar(&o.rehearsalTagConfigFile, "rehearsal-tag-config", "", "Path to the rehearsal tag configuration file.")

if err := fs.Parse(os.Args[1:]); err != nil {
return o, fmt.Errorf("failed to parse flags: %w", err)
Expand Down
105 changes: 102 additions & 3 deletions cmd/pj-rehearse/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type server struct {
gc git.ClientFactory

rehearsalConfig rehearse.RehearsalConfig
tagConfig *rehearse.RehearsalTagConfig
}

func (s *server) helpProvider(_ []prowconfig.OrgRepo) (*pluginhelp.PluginHelp, error) {
Expand All @@ -68,10 +69,10 @@ func (s *server) helpProvider(_ []prowconfig.OrgRepo) (*pluginhelp.PluginHelp, e
Examples: []string{rehearseNormal},
})
pluginHelp.AddCommand(pluginhelp.Command{
Usage: fmt.Sprintf("%s {test-name}", rehearseNormal),
Description: "Run one or more specific rehearsals",
Usage: fmt.Sprintf("%s {test-name|tag}", rehearseNormal),
Description: "Run one or more specific rehearsals, or all rehearsals matching a tag",
WhoCanUse: "Anyone can use on trusted PRs",
Examples: []string{fmt.Sprintf("%s {some-test} {another-test}", rehearseNormal)},
Examples: []string{fmt.Sprintf("%s {some-test} {another-test}", rehearseNormal), fmt.Sprintf("%s tnf", rehearseNormal)},
})
pluginHelp.AddCommand(pluginhelp.Command{
Usage: rehearseAck,
Expand Down Expand Up @@ -127,6 +128,7 @@ func (s *server) helpProvider(_ []prowconfig.OrgRepo) (*pluginhelp.PluginHelp, e
WhoCanUse: "Openshift org members that are not the author of the PR",
Examples: []string{rehearseAllowNetworkAccess},
})

return pluginHelp, nil
}

Expand All @@ -151,10 +153,20 @@ func serverFromOptions(o options) (*server, error) {
rehearsalConfig.ProwjobNamespace = c.ProwJobNamespace
rehearsalConfig.PodNamespace = c.PodNamespace

var tagConfig *rehearse.RehearsalTagConfig
if o.rehearsalTagConfigFile != "" {
var err error
tagConfig, err = rehearse.LoadRehearsalTagConfig(o.rehearsalTagConfigFile)
if err != nil {
return nil, fmt.Errorf("could not load rehearsal tag config: %w", err)
}
}

return &server{
ghc: ghc,
gc: gc,
rehearsalConfig: rehearsalConfig,
tagConfig: tagConfig,
}, nil
}

Expand Down Expand Up @@ -424,6 +436,15 @@ func (s *server) handlePotentialCommands(pullRequest *github.PullRequest, commen
if requestedOnly {
rawJobs := strings.TrimPrefix(command, rehearseNormal+" ")
requestedJobs := strings.Split(rawJobs, " ")

// Check if this is a single tag request
if len(requestedJobs) == 1 && s.tagConfig != nil && s.tagConfig.HasTag(requestedJobs[0]) {
// Handle as tag-based rehearsal
s.rehearseJobs(requestedJobs[0], pullRequest, user, logger)
continue
}

// Handle as regular job names
var unaffected []string
presubmits, periodics, unaffected = rehearse.FilterJobsByRequested(requestedJobs, presubmits, periodics, logger)
if len(unaffected) > 0 {
Expand Down Expand Up @@ -655,6 +676,84 @@ func (s *server) acknowledgeRehearsals(org, repo string, number int, logger *log
}
}

// rehearseJobs handles tag-based job rehearsals
func (s *server) rehearseJobs(tag string, pullRequest *github.PullRequest, user string, logger *logrus.Entry) bool {
org := pullRequest.Base.Repo.Owner.Login
repo := pullRequest.Base.Repo.Name
number := pullRequest.Number

rc := s.rehearsalConfig
repoClient, err := s.getRepoClient(org, repo)
if err != nil {
logger.WithError(err).Error("couldn't create repo client")
return false
}
defer func() {
if err := repoClient.Clean(); err != nil {
logrus.WithError(err).Error("couldn't clean temporary repo folder")
}
}()

candidate, err := s.prepareCandidate(repoClient, pullRequest, logger)
if err != nil {
s.reportFailure("unable prepare a candidate for rehearsal; rehearsals will not be run. This could be due to a branch that needs to be rebased.", err, org, repo, user, number, false, false, logger)
return false
}

allowedLabel := false
approved := false
for _, label := range pullRequest.Labels {
if label.Name == rehearse.NetworkAccessRehearsalsOkLabel {
allowedLabel = true
} else if label.Name == labels.Approved {
approved = true
}
}
networkAccessRehearsalsAllowed := allowedLabel && approved

candidatePath := repoClient.Directory()
presubmits, periodics, _, err := rc.DetermineAffectedJobs(candidate, candidatePath, networkAccessRehearsalsAllowed, logger)
if err != nil {
logger.WithError(err).Error("couldn't determine affected jobs")
s.reportFailure("unable to determine affected jobs", err, org, repo, user, number, true, false, logger)
return false
}

var periodicSlice []prowconfig.Periodic
for _, p := range periodics {
periodicSlice = append(periodicSlice, p)
}
presubmits = rehearse.FilterPresubmitsByTag(presubmits, periodicSlice, s.tagConfig, tag)

if len(presubmits) > 0 {
prConfig, prRefs, presubmitsToRehearse, err := rc.SetupJobs(candidate, candidatePath, presubmits, nil, math.MaxInt, logger)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

math.MaxInt concerns us

if err != nil {
logger.WithError(err).Error("couldn't set up jobs")
s.reportFailure("unable to set up jobs", err, org, repo, user, number, true, false, logger)
return false
}

if err := prConfig.Prow.ValidateJobConfig(); err != nil {
logger.WithError(err).Error("validation of job config failed")
s.reportFailure("config validation failed", err, org, repo, user, number, false, false, logger)
return false
}

success, err := rc.RehearseJobs(candidatePath, prRefs, presubmitsToRehearse, prConfig.Prow, false, logger)
if err != nil {
logger.WithError(err).Error("couldn't rehearse jobs")
s.reportFailure("failed to create rehearsal jobs", err, org, repo, user, number, true, false, logger)
return false
}
return success
} else {
if err := s.ghc.CreateComment(org, repo, number, fmt.Sprintf("@%s: no jobs found matching tag '%s'", user, tag)); err != nil {
logger.WithError(err).Error("failed to create comment")
}
return false
}
}

func (s *server) dumpAffectedJobsToGCS(pullRequest *github.PullRequest, presubmits config.Presubmits, periodics config.Periodics, jobCount int, logger *logrus.Entry) string {
logger.WithField("jobCount", jobCount).Debugf("jobCount is above %d. cannot comment all jobs, writing out to file", s.rehearsalConfig.MaxLimit)
fileContent := []string{"Test Name | Repo | Type | Reason"}
Expand Down
51 changes: 51 additions & 0 deletions pkg/rehearse/tag_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package rehearse

import (
"fmt"
"io/ioutil"

"sigs.k8s.io/yaml"
)

// RehearsalTagConfig is the top-level structure for the tag configuration file.
type RehearsalTagConfig struct {
Tags []Tag `json:"tags"`
}

// Tag defines a single rehearsal tag and its selectors.
type Tag struct {
Name string `json:"name"`
Selectors []Selector `json:"selectors"`
}

// Selector defines the criteria for a job to be included in a tag.
// A job must match at least one selector in a tag's list.
type Selector struct {
JobNamePattern string `json:"job_name_pattern,omitempty"`
FilePathPattern string `json:"file_path_pattern,omitempty"`
ClusterProfile string `json:"cluster_profile,omitempty"`
JobName string `json:"job_name,omitempty"`
}

// LoadRehearsalTagConfig loads a rehearsal tag config from a file.
func LoadRehearsalTagConfig(path string) (*RehearsalTagConfig, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read file %s: %w", path, err)
}
var config RehearsalTagConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("could not unmarshal config: %w", err)
}
return &config, nil
}

// HasTag returns true if the given tag name exists in the configuration.
func (c *RehearsalTagConfig) HasTag(tagName string) bool {
for _, tag := range c.Tags {
if tag.Name == tagName {
return true
}
}
return false
}
94 changes: 94 additions & 0 deletions pkg/rehearse/tag_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package rehearse

import (
"regexp"

"github.com/sirupsen/logrus"

"sigs.k8s.io/prow/pkg/config"

ci_config "github.com/openshift/ci-tools/pkg/config"
)

// FilterPresubmitsByTag filters the given presubmits based on the selectors for the requested tag.
func FilterPresubmitsByTag(presubmits ci_config.Presubmits, periodics []config.Periodic, tagConfig *RehearsalTagConfig, requestedTag string) ci_config.Presubmits {
filtered := make(ci_config.Presubmits)
var targetTag *Tag
for i := range tagConfig.Tags {
if tagConfig.Tags[i].Name == requestedTag {
targetTag = &tagConfig.Tags[i]
break
}
}

if targetTag == nil {
return filtered
}

for repo, jobs := range presubmits {
for _, job := range jobs {
if jobMatchesTag(job.JobBase, targetTag, repo) {
if _, ok := filtered[repo]; !ok {
filtered[repo] = []config.Presubmit{}
}
filtered[repo] = append(filtered[repo], job)
}
}
}

for _, periodic := range periodics {
if jobMatchesTag(periodic.JobBase, targetTag, "") {
logrus.WithField("job", periodic.Name).Warn("Periodic jobs cannot be rehearsed by tag, skipping.")
}
}

return filtered
}

func jobMatchesTag(job config.JobBase, tag *Tag, repo string) bool {
for _, selector := range tag.Selectors {
// Check job name pattern
if selector.JobNamePattern != "" {
re, err := regexp.Compile(selector.JobNamePattern)
if err != nil {
logrus.WithError(err).Warnf("Invalid regex in rehearsal tag selector: %s", selector.JobNamePattern)
continue
}
if re.MatchString(job.Name) {
return true
}
}

// Check exact job name match
if selector.JobName != "" {
if job.Name == selector.JobName {
return true
}
}

// Check cluster profile (stored in job labels)
if selector.ClusterProfile != "" {
if clusterProfile, ok := job.Labels["ci-operator.openshift.io/cloud-cluster-profile"]; ok {
if clusterProfile == selector.ClusterProfile {
return true
}
}
}

// Check file path pattern (matches against the repository path)
if selector.FilePathPattern != "" && repo != "" {
re, err := regexp.Compile(selector.FilePathPattern)
if err != nil {
logrus.WithError(err).Warnf("Invalid regex in rehearsal tag selector: %s", selector.FilePathPattern)
continue
}
// The repo string is in format "org/repo-name", we can match against this
if re.MatchString(repo) {
return true
}
}
}
return false
}

// RehearsalTagConfig contains the mapping of tags to jobs