Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replaced the flake-manager, issue-cacher, and issue-sync with the clu… #2450

Merged
merged 2 commits into from May 4, 2017

Conversation

cjwagner
Copy link
Member

…ster-manager and issue-creator. This change replaces the munger that created issues for individual flakes with a munger that creates issues for flakes based on the clustering data on the triage page.

Design Doc:
https://docs.google.com/a/google.com/document/d/1nnAI8lCCamcluvhh10Bz8vR9moSTyez1dCchU1qnCOI/edit?usp=sharing

@cjwagner cjwagner requested a review from rmmh April 10, 2017 21:17
@k8s-ci-robot
Copy link
Contributor

Thanks for your pull request. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please follow instructions at https://github.com/kubernetes/kubernetes/wiki/CLA-FAQ to sign the CLA.

It may take a couple minutes for the CLA signature to be fully registered; after that, please reply here with a new comment and we'll verify. Thanks.


Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. label Apr 10, 2017
@krzyzacy
Copy link
Member

/assign @rmmh

you'll need to sign the CLA follow the instruction from the bot first.

@fejta
Copy link
Contributor

fejta commented Apr 10, 2017

Is it possible to put the delete of the old munger in its own commit? 4,000 changed lines is extremely challenging to review.

Also cluster makes me think of a kubernetes cluster. Can you choose a different name than cluster-manager?

@fejta fejta self-assigned this Apr 10, 2017
@cjwagner
Copy link
Member Author

@k8s-bot bazel test this

@cjwagner cjwagner force-pushed the cluster-reporter branch 2 times, most recently from 9a0df2c to 2b87217 Compare April 11, 2017 00:53
@cjwagner
Copy link
Member Author

comment

if len(rawIssues) == 0 {
glog.Warningf("IssueCreator found no issues in the repo '%s/%s' authored by %s.\n", creator.config.getOrg(), creator.config.getProject(), creator.authorName)
}
//allocate 10% extra capacity since issues may be added and this list is big so copy is expensive
Copy link
Contributor

Choose a reason for hiding this comment

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

copying a few thousand pointers is not expensive, don't bother with this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

if !creator.refresh() {
glog.Fatalf("Failed to get the list of all issues created by %s in repo '%s/%s' while initing IssueCreator.\n", creator.authorName, creator.config.getOrg(), creator.config.getProject())
}
creator.allIssues_fetchfreq, _ = time.ParseDuration(fmt.Sprintf("%dm", refreshFreqMins))
Copy link
Contributor

Choose a reason for hiding this comment

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

refreshFreqMins * time.Minutes

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

}
}

func (creator *IssueCreator) refresh() bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't call this function every minute, that is prohibitively expensive. issue-cacher.go follows the correct pattern of doing the enumeration once, and then updating using changed issues using the Munge() method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

//this type wraps the github.Config type so that it can match the RepoClient interface
type githubConfig github.Config

//gets the underlying *github.Config
Copy link
Contributor

@rmmh rmmh Apr 11, 2017

Choose a reason for hiding this comment

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

Throughout: these comments should be complete sentences starting with the function name, to make godoc work.

"RealConfig returns the underlying github.Config."

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@rmmh
Copy link
Contributor

rmmh commented Apr 11, 2017

I think you should use issue-cacher.go instead of deleting it. This new "refresh every minute" implementation will slow down the entire munger.

All that should be required is changing issue-cacher.go's keyFromIssue to extract cluster IDs from issue bodies when present.

@fejta
Copy link
Contributor

fejta commented Apr 12, 2017

I remain unconvinced that we want a monolithic munger binary verses a bunch of small focused programs that prow triggers on a periodic basis

@fejta
Copy link
Contributor

fejta commented Apr 12, 2017

I do not think this program needs to run more than once per day

@thelinuxfoundation
Copy link

bump the CLA check

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. and removed cncf-cla: no Indicates the PR's author has not signed the CNCF CLA. labels Apr 12, 2017
var assignee *string
if owner != "" {
assignee = &owner
if len(owners) == 0 {
Copy link
Contributor

@rmmh rmmh Apr 13, 2017

Choose a reason for hiding this comment

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

This is not quite correct. You want a nil assignees pointer by default, and set it to the address of owners if it's not empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like there are still a bunch of comments that have not been responded to.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. (Used empty slice since nil doesn't behave well with the github client).

return matchCount == 1
}

//We also need a dummy Issue implementation
Copy link
Contributor

Choose a reason for hiding this comment

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

unnecessary comment

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

priority int //-1 indicates no priority
}

func (issue *fakeIssue) Title() string {
Copy link
Contributor

@rmmh rmmh Apr 13, 2017

Choose a reason for hiding this comment

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

should be func (i *fakeIssue) Title() string { (throughout)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

return nil
}

//checks that exactly 1 issue in fc.issues matches the parameters and that no
Copy link
Contributor

@rmmh rmmh Apr 13, 2017

Choose a reason for hiding this comment

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

Verify checks... (and end with a period)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

}

func TestIssueCreator(tt *testing.T) {
t = tt
Copy link
Contributor

Choose a reason for hiding this comment

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

just rename the function parameter

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. (made t a field instead of a global)

for i := 0; i < len(issue.Assignees); i++ {
assignees[i] = *issue.Assignees[i].Login
}
if !stringSlicesEqual(assignees, owners) {
Copy link
Contributor

Choose a reason for hiding this comment

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

use reflect.deepEqual instead

Copy link
Contributor

Choose a reason for hiding this comment

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

Boo!

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -111,3 +111,21 @@ func TestIssueUsersAll(t *testing.T) {
t.Errorf("AllUsers (%s) doesn't match expected list: %s", users.AllUsers().List(), expected)
}
}

func TestFetchWebFileContents(t *testing.T) {
goodurl := "https://storage.googleapis.com/k8s-gubernator/triage/failure_data.json"
Copy link
Contributor

Choose a reason for hiding this comment

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

In general, don't write tests that require external network connectivity.

You can use net/http/httptest if you really need to test this function. Otherwise I wouldn't bother-- it's pretty trivial.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove the else { }

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

glog.Fatalln("ERROR: ", err)
}
//ensure that this dict has the keys it should
if _, exists := buildsDict[keyColumns]; !exists {
Copy link
Contributor

Choose a reason for hiding this comment

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

_, ok is more idiomatic Go

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

var clusters []*Cluster

//first parse the "clustered" and "builds" keys from the top level
topLevelDict := make(map[string]json.RawMessage)
Copy link
Contributor

@rmmh rmmh Apr 13, 2017

Choose a reason for hiding this comment

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

Can you unmarshal to a struct instead of a map? You can then test each of the members for being non-empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

//load job and build data
allJobs := loadJobData(topLevelDict[keyBuilds])
//only consider failures that occured after this time
cutoffTime := time.Now().AddDate(0, 0, -1*filer.windowDays).Unix()
Copy link
Contributor

Choose a reason for hiding this comment

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

-filer.WindowDays

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

glog.Fatalln("ERROR: ", err)
}
for _, cluster := range rawClusters {
clust := new(Cluster)
Copy link
Contributor

Choose a reason for hiding this comment

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

clust := &Cluster{filer: filer, testFailed: ... }

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

topLevelDict := make(map[string]json.RawMessage)
err := json.Unmarshal(jsonIn, &topLevelDict)
if err != nil {
glog.Fatalln("ERROR: ", err)
Copy link
Contributor

@rmmh rmmh Apr 13, 2017

Choose a reason for hiding this comment

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

Don't use Fatal in production code unless it's actually unrecoverable. The SQ going offline is very disruptive. Log an error and return nil, or make parseClusters return ([]*Cluster, err).

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


//Munge updates the IssueCreator's cache of issues if the munge object provided is an issue authored by the currently authenticated user.
func (creator *IssueCreator) Munge(obj *github.MungeObject) {
//ignore pull requests
Copy link
Contributor

Choose a reason for hiding this comment

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

none of the comments in this function are necessary

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

//Initialize prepares an IssueCreator for use by other mungers.
//This includes determining the currently authenticated user, fetching all issues created by that user
//from github, fetching the labels that are valid for the repo, and initializing the test owner and sig data.
func (creator *IssueCreator) Initialize(config *github.Config, feats *features.Features) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

throughout: the method signature should be func (c *IssueCreator) or func (ic *IssueCreator). Go style prefers terse method receivers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

authorName string
//allIssues is a local cache of all issues in the repo authored by the currently authenticated user.
//MungeObjects are keyed by issue number
allIssues map[int]*github.MungeObject
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could be map[int]*github.Issue without losing anything-- I think you only access the .Issue field of the mungeobject. That would also remove the need for the somewhat strange NewEmptyMungeObject function.

Copy link
Member Author

Choose a reason for hiding this comment

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

I originally did have that map type, but I switched it to MungeObject so that an Issue interface implementation can get more info about the github issues if it needs it. Specifically I was thinking that it might be useful to retrieve the comments for the issue which is a functionality provided by MungeObject. Should I switch it back?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. (switched back to *githubapi.Issue value type)

@@ -0,0 +1,19 @@
name,owner,auto-assigned,sig
Copy link
Contributor

Choose a reason for hiding this comment

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

delete this file entirely, now that it's unused

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -112,3 +115,20 @@ func IsValidUser(u *github.User) bool {
func IsMungeBot(u *github.User) bool {
return IsValidUser(u) && *u.Login == BotName
}

//fetches raw data from a url (used to fetch json file)
func FetchWebFileContents(url string) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to "ReadHTTP" for symmetry with bytes.ReadFile and fix the comment. You can also make submit-queue-batch.go:getJobs use this instead. :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

fmt.Fprintf(&buf, "Failure stats over time range '%s' to '%s'\n", cutoffTime.Format(time.RFC822), time.Now().Format(time.RFC822))
//totals
fmt.Fprintf(&buf, "%d tests failed, %d jobs failed, %d builds failed.\n###### Top failed tests by jobs failed:\n", clust.totalTests, clust.totalJobs, clust.totalBuilds)
//build top tests failed info
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

//build top jobs failed info
fmt.Fprintln(&buf, "###### Top failed jobs by builds failed:")
for _, job := range clust.topJobsFailed(clust.filer.topJobsCount) {
fmt.Fprintf(&buf, "%s: %d builds\n", job, len(clust.jobsFailed[job].builds))
Copy link
Contributor

Choose a reason for hiding this comment

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

linking directly to an example failure for each of the failed jobs would be helpful

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

if len(closedIssues) > 0 {
fmt.Fprintln(&buf, "###### Previously issues for this cluster:")
for _, closed := range closedIssues {
fmt.Fprintln(&buf, closed.Issue.HTMLURL)
Copy link
Contributor

Choose a reason for hiding this comment

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

fmt.Fprintf(&buf, "#%d ", closed.Issue.Number) should display better.

Copy link
Member Author

Choose a reason for hiding this comment

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

I made the changes you requested. Ready for you to review again.


//now we know we need to make a new issue
var buf bytes.Buffer
fmt.Fprintf(&buf, "#### Cluster [%s] More data at: %s#%s\n", clust.ID(), clust.filer.triageURL, clust.ID())
Copy link
Contributor

Choose a reason for hiding this comment

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

Turn "more data" into a link: fmt.Fprintf(&buf, "#### Cluster [%s] [more data](%s#%s)\n", clust.ID(), clust.filer.triageURL, clust.ID())

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

//now we know we need to make a new issue
var buf bytes.Buffer
fmt.Fprintf(&buf, "#### Cluster [%s] More data at: %s#%s\n", clust.ID(), clust.filer.triageURL, clust.ID())
fmt.Fprintf(&buf, "Failure stats over time range '%s' to '%s'\n", cutoffTime.Format(time.RFC822), time.Now().Format(time.RFC822))
Copy link
Contributor

Choose a reason for hiding this comment

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

again, the time range should be specified based on what's in the data file, not the current time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

flake-manager.test-owners-csv: /gitrepos/kubernetes/test/test_owners.csv
triage-filer.test-owners-csv: /gitrepos/kubernetes/test/test_owners.csv
triage-filer.windowdays: "5"
triage-filer.synccount: "10"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we please actually start with 3 or 5 instead of 10? I'm worried 10 issues every day will be too many. I'd much rather have too few issues than too many.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -41,7 +41,9 @@ data:
# munger specific options.
path-label.path-label-config: ""
block-path.block-path-config: ""
flake-manager.test-owners-csv: ""
triage-filer.test-owners-csv: ""
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure this makes sense... Is there a triage page for test-infra failures?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -42,7 +42,9 @@ data:
# munger specific options.
path-label.path-label-config: ""
block-path.block-path-config: ""
flake-manager.test-owners-csv: ""
triage-filer.test-owners-csv: ""
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as test-infra comment

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

ListAllIssues(options *githubapi.IssueListByRepoOptions) ([]*githubapi.Issue, error)
NewIssue(title, body string, labels, owners []string) (*github.MungeObject, error)

getDryRun() bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the correct golang pattern? I would expect isDryRun()

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

creator.config = RepoClient(&cfg)

//load test owner/SIG data
var err error
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason why you are doing this over creator.owners, err := testowner.NewReloading() and if err := creator.load(); err != nil?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ack. (Cannot define a new variable and assign to a field in a single assignment statement in go)

func (creator *IssueCreator) loadCache() error {
//try to get the current authenticated user
user, err := creator.config.GetUser("")
if err != nil || user == nil || user.Login == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect it will be more helpful to return a specific error message for each of these scenarios.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

owners := issue.Owners()
labels := issue.Labels()
if prio, ok := issue.Priority(); ok {
labels = append(labels, "priority/P"+strconv.Itoa(prio))
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think these labels are deprecated. Can we just skip setting a priority? Alternatively use the new priority/random-annoying-string labels

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Priorities are not currently used but are now of type string instead of an int.

cmd.Flags().IntVar(&filer.topClustersCount, "triagesynccount", 10, "The number of clusters to sync issues for on github.")
cmd.Flags().StringVar(&filer.triageURL, "triageURL", "https://go.k8s.io/triage", "The url prefix to add before cluster hash to create a link to the triage page for a specific cluster.")
cmd.Flags().IntVar(&filer.windowDays, "triagewindowdays", 5, "The size of the sliding time window (in days) that is used to determine which failures to consider.")
cmd.Flags().IntVar(&filer.syncFreqMins, "triagesyncfreq", 60, "The frequency at which to run the TriageFiler in minutes.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Would duration be a better fit here? https://golang.org/pkg/flag/#Duration

Also can you default this to once a day? I really want to minimize the number of issues we automatically file. Said this elsewhere but too few auto-filed issues is WAY better than too many -- since too many just overwhelms people and they give up, reducing the value of all issues.

Copy link
Member Author

Choose a reason for hiding this comment

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

I made the requested changes. Ready to be reviewed again.

@cjwagner cjwagner force-pushed the cluster-reporter branch 2 times, most recently from 1a71ca5 to 1a8eef7 Compare April 21, 2017 02:15
@fejta
Copy link
Contributor

fejta commented Apr 25, 2017

FYI when you are ready for another round of reviews please leave a reply on each of the comments (Done, Ack, or anything else, really) to let us know to review again.

Copy link
Contributor

@fejta fejta left a comment

Choose a reason for hiding this comment

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

The size of this PR makes my head hurt

}

// AddFlags will add any requested flags to the cobra `cmd`.
func (f *TriageFiler) AddFlags(cmd *cobra.Command, config *github.Config) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts:

  • Is it important to start with these as flags rather than consts? It is cheap/trivial to release a new version.

  • Should all of these start with triage?

  • Shouldn't the words be separated with hyphens? Aka cluster-data-url &c

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed all but the 2 most important flags to constants. I kept the triage-sync-count (the number of failure clusters to sync) and triage-window-days (the size of the sliding window) flags since we may want to mess with these frequently at first.
Both of the names start with triage and use hyphens now.

return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably automatically retry 500 level errors a few times

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@cjwagner cjwagner force-pushed the cluster-reporter branch 2 times, most recently from d0c225c to ad1b242 Compare May 2, 2017 20:07
@rmmh
Copy link
Contributor

rmmh commented May 3, 2017

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 3, 2017
@cjwagner cjwagner merged commit 2c8f741 into kubernetes:master May 4, 2017
@cjwagner cjwagner deleted the cluster-reporter branch May 4, 2017 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants