Skip to content
Merged
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
github.com/go-logr/zapr v0.1.1 // indirect
github.com/go-openapi/spec v0.19.2
github.com/go-sql-driver/mysql v0.0.0-20160411075031-7ebe0a500653 // indirect
github.com/go-test/deep v1.0.4
github.com/gobuffalo/envy v1.6.15 // indirect
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+
github.com/go-sql-driver/mysql v0.0.0-20160411075031-7ebe0a500653 h1:cRYCw9putX9pmpNdGkE/rWrY6gNGqZvdgGT1RPl6K18=
github.com/go-sql-driver/mysql v0.0.0-20160411075031-7ebe0a500653/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gobuffalo/envy v1.6.5 h1:X3is06x7v0nW2xiy2yFbbIjwHz57CD6z6MkvqULTCm8=
Expand Down
1 change: 0 additions & 1 deletion prow/cmd/tide/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ go_library(
deps = [
"//pkg/flagutil:go_default_library",
"//pkg/io:go_default_library",
"//prow/apis/prowjobs/v1:go_default_library",
"//prow/config:go_default_library",
"//prow/config/secret:go_default_library",
"//prow/flagutil:go_default_library",
Expand Down
14 changes: 4 additions & 10 deletions prow/cmd/tide/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import (

"k8s.io/test-infra/pkg/flagutil"
"k8s.io/test-infra/pkg/io"
prowjobsv1 "k8s.io/test-infra/prow/apis/prowjobs/v1"
"k8s.io/test-infra/prow/config"
"k8s.io/test-infra/prow/config/secret"
prowflagutil "k8s.io/test-infra/prow/flagutil"
Expand Down Expand Up @@ -171,26 +170,21 @@ func main() {
if err != nil {
logrus.WithError(err).Fatal("Error constructing mgr.")
}
// Make sure the manager creates a cache for ProwJobs by requesting an informer
if _, err := mgr.GetCache().GetInformer(&prowjobsv1.ProwJob{}); err != nil {
logrus.WithError(err).Fatal("Error getting ProwJob informer.")
c, err := tide.NewController(githubSync, githubStatus, mgr, cfg, gitClient, o.maxRecordsPerPool, opener, o.historyURI, o.statusURI, nil)
if err != nil {
logrus.WithError(err).Fatal("Error creating Tide controller.")
}

interrupts.Run(func(ctx context.Context) {
if err := mgr.Start(ctx.Done()); err != nil {
logrus.WithError(err).Fatal("Mgr failed.")
}
logrus.Info("Mgr finished gracefully.")
})
mgrSyncCtx, mgrSyncCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer mgrSyncCtxCancel()
if synced := mgr.GetCache().WaitForCacheSync(mgrSyncCtx.Done()); !synced {
logrus.Fatal("Timed out waiting for cachesync")
}

c, err := tide.NewController(githubSync, githubStatus, mgr.GetClient(), cfg, gitClient, o.maxRecordsPerPool, opener, o.historyURI, o.statusURI, nil)
if err != nil {
logrus.WithError(err).Fatal("Error creating Tide controller.")
}
interrupts.OnInterrupt(func() {
c.Shutdown()
if err := gitClient.Clean(); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions prow/tide/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ go_library(
"@com_github_shurcool_githubv4//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_apimachinery//pkg/util/sets:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client:go_default_library",
"@io_k8s_sigs_yaml//:go_default_library",
Expand Down Expand Up @@ -63,12 +64,15 @@ go_test(
"//prow/github:go_default_library",
"//prow/tide/blockers:go_default_library",
"//prow/tide/history:go_default_library",
"@com_github_go_test_deep//:go_default_library",
"@com_github_shurcool_githubv4//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@io_k8s_apimachinery//pkg/api/equality:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
"@io_k8s_apimachinery//pkg/util/diff:go_default_library",
"@io_k8s_apimachinery//pkg/util/sets:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client:go_default_library",
"@io_k8s_sigs_controller_runtime//pkg/client/fake:go_default_library",
],
)
89 changes: 67 additions & 22 deletions prow/tide/tide.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
githubql "github.com/shurcooL/githubv4"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -203,15 +204,21 @@ func init() {
prometheus.MustRegister(tideMetrics.poolErrors)
}

type manager interface {
GetClient() ctrlruntimeclient.Client
GetFieldIndexer() ctrlruntimeclient.FieldIndexer
}

// NewController makes a Controller out of the given clients.
func NewController(ghcSync, ghcStatus github.Client, prowJobClient ctrlruntimeclient.Client, cfg config.Getter, gc *git.Client, maxRecordsPerPool int, opener io.Opener, historyURI, statusURI string, logger *logrus.Entry) (*Controller, error) {
func NewController(ghcSync, ghcStatus github.Client, mgr manager, cfg config.Getter, gc *git.Client, maxRecordsPerPool int, opener io.Opener, historyURI, statusURI string, logger *logrus.Entry) (*Controller, error) {
if logger == nil {
logger = logrus.NewEntry(logrus.StandardLogger())
}
hist, err := history.New(maxRecordsPerPool, opener, historyURI)
if err != nil {
return nil, fmt.Errorf("error initializing history client from %q: %v", historyURI, err)
}

sc := &statusController{
logger: logger.WithField("controller", "status-update"),
ghc: ghcStatus,
Expand All @@ -223,11 +230,30 @@ func NewController(ghcSync, ghcStatus github.Client, prowJobClient ctrlruntimecl
path: statusURI,
}
go sc.run()
return newSyncController(logger, ghcSync, mgr, cfg, gc, sc, hist)
}

func newSyncController(
logger *logrus.Entry,
ghcSync githubClient,
mgr manager,
cfg config.Getter,
gc *git.Client,
sc *statusController,
hist *history.History,
) (*Controller, error) {
if err := mgr.GetFieldIndexer().IndexField(
&prowapi.ProwJob{},
cacheIndexName,
cacheIndexFunc,
); err != nil {
return nil, fmt.Errorf("failed to add baseSHA index to cache: %v", err)
}
return &Controller{
ctx: context.Background(),
logger: logger.WithField("controller", "sync"),
ghc: ghcSync,
prowJobClient: prowJobClient,
prowJobClient: mgr.GetClient(),
config: cfg,
gc: gc,
sc: sc,
Expand Down Expand Up @@ -308,19 +334,9 @@ func (c *Controller) Sync() error {
"duration", time.Since(start).String(),
).Debugf("Found %d (unfiltered) pool PRs.", len(prs))

var pjs []prowapi.ProwJob
var blocks blockers.Blockers
var err error
if len(prs) > 0 {
start := time.Now()
pjList := &prowapi.ProwJobList{}
if err := c.prowJobClient.List(c.ctx, pjList, ctrlruntimeclient.InNamespace(c.config().ProwJobNamespace)); err != nil {
c.logger.WithField("duration", time.Since(start).String()).Debug("Failed to list ProwJobs from the cluster.")
return err
}
c.logger.WithField("duration", time.Since(start).String()).Debug("Listed ProwJobs from the cluster.")
pjs = pjList.Items

if label := c.config().Tide.BlockerLabel; label != "" {
c.logger.Debugf("Searching for blocking issues (label %q).", label)
orgExcepts, repos := c.config().Tide.Queries.OrgExceptionsAndRepos()
Expand All @@ -336,7 +352,7 @@ func (c *Controller) Sync() error {
}
}
// Partition PRs into subpools and filter out non-pool PRs.
rawPools, err := c.dividePool(prs, pjs)
rawPools, err := c.dividePool(prs)
if err != nil {
return err
}
Expand Down Expand Up @@ -1382,7 +1398,7 @@ func poolKey(org, repo, branch string) string {

// dividePool splits up the list of pull requests and prow jobs into a group
// per repo and branch. It only keeps ProwJobs that match the latest branch.
func (c *Controller) dividePool(pool map[string]PullRequest, pjs []prowapi.ProwJob) (map[string]*subpool, error) {
func (c *Controller) dividePool(pool map[string]PullRequest) (map[string]*subpool, error) {
sps := make(map[string]*subpool)
for _, pr := range pool {
org := string(pr.Repository.Owner.Login)
Expand Down Expand Up @@ -1410,15 +1426,19 @@ func (c *Controller) dividePool(pool map[string]PullRequest, pjs []prowapi.ProwJ
}
sps[fn].prs = append(sps[fn].prs, pr)
}
for _, pj := range pjs {
if pj.Spec.Type != prowapi.PresubmitJob && pj.Spec.Type != prowapi.BatchJob {
continue
}
fn := poolKey(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef)
if sps[fn] == nil || pj.Spec.Refs.BaseSHA != sps[fn].sha {
continue

for subpoolkey, sp := range sps {
pjs := &prowapi.ProwJobList{}
err := c.prowJobClient.List(
c.ctx,
pjs,
ctrlruntimeclient.MatchingField(cacheIndexName, cacheIndexKey(sp.org, sp.repo, sp.branch, sp.sha)),
ctrlruntimeclient.InNamespace(c.config().ProwJobNamespace))
if err != nil {
return nil, fmt.Errorf("failed to list jobs for subpool %s: %v", subpoolkey, err)
}
sps[fn].pjs = append(sps[fn].pjs, pj)
c.logger.WithField("subpool", subpoolkey).Debugf("Found %d prowjobs.", len(pjs.Items))
sps[subpoolkey].pjs = pjs.Items
Copy link
Member

Choose a reason for hiding this comment

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

The fact that the base SHA matches is not enough to know that this PJ matches the tide pool. It is possible for the same base sha to exist in different org/repo. More realistically, it is also possible for two different branches (BaseRef) to point to the same commit and we need to distinguish the different pools in this case.

Copy link
Member

Choose a reason for hiding this comment

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

We don't necessarily need to make the index more complicated, but if we keep it as is we need to filter out PJs for other subpools like we did here before.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have unit tests for these scenarios? That's the best way to ensure it remains invariant.

Copy link
Member

Choose a reason for hiding this comment

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

Alvaro added unit tests, but they require the envtest package I asked about here: #14830 (review)

(Also if this is what we want to do, do you have any recommendations on the best way to do this with bazel?)

Copy link
Contributor

@fejta fejta Oct 22, 2019

Choose a reason for hiding this comment

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

Can we add actual unit tests? I don't see any reason why we would need etcd in order to validate the behavior of

  • the same SHA existing in multiple repos
  • two branches pointing to the same commit

AKA ensuring

  • commit C repo R, branch B => index i1
  • commit C, repo S, branch B => index i2
  • commit C, repo R, branch D => index i3
    (or at least that none of these equals the other) seems sufficient here

This seems more useful/important than validating the internal behavior of c.prowJobClient.List() (that would be more an integration test than unit test)

Copy link
Member Author

Choose a reason for hiding this comment

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

We validate that "input X return exactly Y". This is different than "ensure input X and X' return whatever values so long as they differ from each other", right?

Added a TestCacheIndexFuncReturnsDifferentResultsForDifferentInputs that verifies that.

Generally, using a cache with an index is probably the most efficient solution for filtering kube api objects repeatedly, so it would be great if we can find a way to test that that is agreeable for everyone :) How do you feel about using envtest for that @fejta ?

Copy link
Contributor

@fejta fejta Oct 23, 2019

Choose a reason for hiding this comment

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

Faking the interface seems like the most appropriate strategy here.

Making everything depend on making real calls to the apiserver is an inefficient test strategy. We're just consumers of all this code. Why can't we just assume kubernetes works correctly?

We do not, for example, validate the the Google cloud storage client transfers files correctly. We just assume it works and fake the return values. This is efficient and hasn't lead to any regressions.

I would recommend this faking strategy here over replacing unit testing with integration testing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay. I've also opened kubernetes-sigs/controller-runtime#657 for this, because ideally we don't have to build fakes downstream.

While a test for "do we actually add the index to the cache" definitely makes sense, adding a fake client that uses an index pretty much only tests our dependencies (that do have tests for this) and our fake implementation. For the sake of getting this done, I'll add it nonetheless.

Copy link
Contributor

@fejta fejta Oct 23, 2019

Choose a reason for hiding this comment

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

I agree that is probably more work than is justified? This is the call to test:

		err := c.prowJobClient.List(
			c.ctx,
			pjs,
			ctrlruntimeclient.MatchingField(cacheIndexName, cacheIndexKey(sp.org, sp.repo, sp.branch, sp.sha)),
			ctrlruntimeclient.InNamespace(c.config().ProwJobNamespace))

So wrap it in a function:

type lister interface {
  List(context.Context, *prowapi.ProwJobList, matchingFieldRetValue, namespaceRetValue) error
}

func listMatchingJobs(ctx context.Context, prowJobClient lister, sp subpool, namespace string) (*prowapi.ProwJobList, error) {
  var pjs prowapi.ProwJobList
  if err := prowJobClient.List(ctx, &pjs, MatchingField(cacheIndexName, cacheIndexKey(sp.org, sp.repo, sp.branch, sp.sha)), InNamespace(namespace); err != nil {
    return nil, err
  }
  return &pjs

Now write a fakeLister and validate that we send cacheIndexName, cacheIndexKey and namespace correctly.

Change all the list calls to this helper function.

Done.

I don't think creating a fake client that works correctly is super necessary here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well I ended up writing a small implementation of the subset of the manager we need and something that just embedds a an upstream fakeclient and wraps its List call to use an index func if requested.

I think this should be good now, PTAL.

}
return sps, nil
}
Expand Down Expand Up @@ -1571,3 +1591,28 @@ func orgRepoQueryString(orgs, repos []string, orgExceptions map[string]sets.Stri
}
return strings.Join(toks, " ")
}

// cacheIndexName is the name of the index that indexes presubmit+batch ProwJobs by
// org+repo+branch+baseSHA. Use the cacheIndexKey func to get the correct key.
const cacheIndexName = "tide-global-index"

// cacheIndexKey returns the index key for the tideCacheIndex
func cacheIndexKey(org, repo, branch, baseSHA string) string {
return fmt.Sprintf("%s/%s:%s@%s", org, repo, branch, baseSHA)
}

func cacheIndexFunc(obj runtime.Object) []string {
pj, ok := obj.(*prowapi.ProwJob)
// Should never happen
if !ok {
return nil
}
// We do not care about jobs other than presubmit and batch
if pj.Spec.Type != prowapi.PresubmitJob && pj.Spec.Type != prowapi.BatchJob {
return nil
}
if pj.Spec.Refs == nil {
return nil
}
return []string{cacheIndexKey(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef, pj.Spec.Refs.BaseSHA)}
}
Loading