diff --git a/Makefile b/Makefile index 4e37b1a575..2a87f19f67 100644 --- a/Makefile +++ b/Makefile @@ -747,9 +747,9 @@ site: site/themes/docsy/assets/vendor/bootstrap/package.js out/hugo/hugo ## Serv out/mkcmp: GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ cmd/performance/mkcmp/main.go -.PHONY: out/performance-monitor -out/performance-monitor: - GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ cmd/performance/monitor/monitor.go +.PHONY: out/performance-bot +out/performance-bot: + GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ cmd/performance/pr-bot/bot.go .PHONY: compare compare: out/mkcmp out/minikube diff --git a/cmd/minikube/cmd/delete.go b/cmd/minikube/cmd/delete.go index 33a31488c1..6b50e46316 100644 --- a/cmd/minikube/cmd/delete.go +++ b/cmd/minikube/cmd/delete.go @@ -91,7 +91,6 @@ func init() { if err := viper.BindPFlags(deleteCmd.Flags()); err != nil { exit.Error(reason.InternalBindFlags, "unable to bind flags", err) } - RootCmd.AddCommand(deleteCmd) } // shotgun cleanup to delete orphaned docker container data diff --git a/cmd/minikube/cmd/start.go b/cmd/minikube/cmd/start.go index 10f9c8636f..1803210ccc 100644 --- a/cmd/minikube/cmd/start.go +++ b/cmd/minikube/cmd/start.go @@ -829,7 +829,7 @@ func memoryLimits(drvName string) (int, int, error) { if err != nil { return -1, -1, err } - containerLimit = int(s.TotalMemory / 1024 / 1024) + containerLimit = util.ConvertBytesToMB(s.TotalMemory) } return sysLimit, containerLimit, nil diff --git a/cmd/minikube/cmd/stop.go b/cmd/minikube/cmd/stop.go index 72dbce79a0..dc595cff91 100644 --- a/cmd/minikube/cmd/stop.go +++ b/cmd/minikube/cmd/stop.go @@ -60,8 +60,6 @@ func init() { if err := viper.GetViper().BindPFlags(stopCmd.Flags()); err != nil { exit.Error(reason.InternalFlagsBind, "unable to bind flags", err) } - - RootCmd.AddCommand(stopCmd) } // runStop handles the executes the flow of "minikube stop" diff --git a/cmd/performance/pr-bot/bot.go b/cmd/performance/pr-bot/bot.go index 174c10d9e4..340c4ef8f4 100644 --- a/cmd/performance/pr-bot/bot.go +++ b/cmd/performance/pr-bot/bot.go @@ -18,12 +18,17 @@ package main import ( "context" + "fmt" "log" "time" + + "github.com/pkg/errors" + "k8s.io/minikube/pkg/perf/monitor" ) func main() { for { + log.Print("~~~~~~~~~ Starting performance analysis ~~~~~~~~~~~~~~") if err := analyzePerformance(context.Background()); err != nil { log.Printf("error executing performance analysis: %v", err) } @@ -36,5 +41,32 @@ func main() { // 2. running mkcmp against those PRs // 3. commenting results on those PRs func analyzePerformance(ctx context.Context) error { + client := monitor.NewClient(ctx, monitor.GithubOwner, monitor.GithubRepo) + prs, err := client.ListOpenPRsWithLabel(monitor.OkToTestLabel) + if err != nil { + return errors.Wrap(err, "listing open prs") + } + log.Print("got prs:", prs) + for _, pr := range prs { + log.Printf("~~~ Analyzing PR %d ~~~", pr) + newCommitsExist, err := client.NewCommitsExist(pr, monitor.BotName) + if err != nil { + return err + } + if !newCommitsExist { + log.Println("New commits don't exist, skipping rerun...") + continue + } + var message string + message, err = monitor.RunMkcmp(ctx, pr) + if err != nil { + message = fmt.Sprintf("Error: %v\n%s", err, message) + } + log.Printf("message for pr %d:\n%s\n", pr, message) + if err := client.CommentOnPR(pr, message); err != nil { + return err + } + log.Print("successfully commented on PR") + } return nil } diff --git a/images/logo/logo_white.svg b/images/logo/logo_white.svg index c932459ae6..1937fdc8b1 100644 --- a/images/logo/logo_white.svg +++ b/images/logo/logo_white.svg @@ -1,6 +1,6 @@ - - + + minikube Created with Sketch. diff --git a/pkg/drivers/common.go b/pkg/drivers/common.go index 2fde2efde8..8c5c98bc73 100644 --- a/pkg/drivers/common.go +++ b/pkg/drivers/common.go @@ -29,6 +29,7 @@ import ( "github.com/docker/machine/libmachine/ssh" "github.com/golang/glog" "github.com/pkg/errors" + "k8s.io/minikube/pkg/util" ) // This file is for common code shared among internal machine drivers @@ -74,7 +75,7 @@ func createRawDiskImage(sshKeyPath, diskPath string, diskSizeMb int) error { return errors.Wrapf(err, "closing file %s", diskPath) } - if err := os.Truncate(diskPath, int64(diskSizeMb*1000000)); err != nil { + if err := os.Truncate(diskPath, util.ConvertMBToBytes(diskSizeMb)); err != nil { return errors.Wrap(err, "truncate") } return nil diff --git a/pkg/drivers/common_test.go b/pkg/drivers/common_test.go index cc250e03de..14cc631082 100644 --- a/pkg/drivers/common_test.go +++ b/pkg/drivers/common_test.go @@ -36,7 +36,7 @@ func Test_createDiskImage(t *testing.T) { diskPath := filepath.Join(tmpdir, "disk") sizeInMb := 100 - sizeInBytes := int64(sizeInMb) * 1000000 + sizeInBytes := int64(104857600) if err := createRawDiskImage(sshPath, diskPath, sizeInMb); err != nil { t.Errorf("createDiskImage() error = %v", err) } diff --git a/pkg/drivers/kvm/domain.go b/pkg/drivers/kvm/domain.go index 2d8a18d5fa..3a69cc0b93 100644 --- a/pkg/drivers/kvm/domain.go +++ b/pkg/drivers/kvm/domain.go @@ -31,8 +31,8 @@ import ( const domainTmpl = ` - {{.MachineName}} - {{.Memory}} + {{.MachineName}} + {{.Memory}} {{.CPU}} diff --git a/pkg/minikube/machine/info.go b/pkg/minikube/machine/info.go index 99bf55982d..4305a5a143 100644 --- a/pkg/minikube/machine/info.go +++ b/pkg/minikube/machine/info.go @@ -29,6 +29,7 @@ import ( "k8s.io/minikube/pkg/minikube/out" "k8s.io/minikube/pkg/minikube/out/register" "k8s.io/minikube/pkg/minikube/style" + "k8s.io/minikube/pkg/util" ) // HostInfo holds information on the user's machine @@ -38,10 +39,6 @@ type HostInfo struct { DiskSize int64 } -func megs(bytes uint64) int64 { - return int64(bytes / 1024 / 1024) -} - // CachedHostInfo returns system information such as memory,CPU, DiskSize func CachedHostInfo() (*HostInfo, error, error, error) { var cpuErr, memErr, diskErr error @@ -61,8 +58,8 @@ func CachedHostInfo() (*HostInfo, error, error, error) { var info HostInfo info.CPUs = len(i) - info.Memory = megs(v.Total) - info.DiskSize = megs(d.Total) + info.Memory = util.ConvertUnsignedBytesToMB(v.Total) + info.DiskSize = util.ConvertUnsignedBytesToMB(d.Total) return &info, cpuErr, memErr, diskErr } diff --git a/pkg/minikube/perf/logs_test.go b/pkg/minikube/perf/logs_test.go index b62e0cb295..0fbdf35bfb 100644 --- a/pkg/minikube/perf/logs_test.go +++ b/pkg/minikube/perf/logs_test.go @@ -38,7 +38,8 @@ func TestTimeCommandLogs(t *testing.T) { if !ok { t.Fatalf("expected log %s but didn't find it", log) } - if actualTime < time { + // Let's give a little wiggle room so we don't fail if time is 3 and actualTime is 2.999 + if actualTime < time && time-actualTime > 0.001 { t.Fatalf("expected log \"%s\" to take more time than it actually did. got %v, expected > %v", log, actualTime, time) } } diff --git a/pkg/perf/monitor/constants.go b/pkg/perf/monitor/constants.go new file mode 100644 index 0000000000..be4a0011ab --- /dev/null +++ b/pkg/perf/monitor/constants.go @@ -0,0 +1,25 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +const ( + GithubAccessTokenEnvVar = "GITHUB_ACCESS_TOKEN" + OkToTestLabel = "ok-to-test" + GithubOwner = "kubernetes" + GithubRepo = "minikube" + BotName = "minikube-pr-bot" +) diff --git a/pkg/perf/monitor/execute.go b/pkg/perf/monitor/execute.go new file mode 100644 index 0000000000..543ed0114c --- /dev/null +++ b/pkg/perf/monitor/execute.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" +) + +// RunMkcmp runs minikube built at the given pr against minikube at master +func RunMkcmp(ctx context.Context, pr int) (string, error) { + // run 'git pull' so that minikube dir is up to date + if _, err := runCmdInMinikube(ctx, []string{"git", "pull", "origin", "master"}); err != nil { + return "", errors.Wrap(err, "running git pull") + } + mkcmpPath := "out/mkcmp" + minikubePath := "out/minikube" + if _, err := runCmdInMinikube(ctx, []string{"make", mkcmpPath, minikubePath}); err != nil { + return "", errors.Wrap(err, "building minikube and mkcmp at head") + } + return runCmdInMinikube(ctx, []string{mkcmpPath, minikubePath, fmt.Sprintf("pr://%d", pr)}) +} + +// runCmdInMinikube runs the cmd and return stdout +func runCmdInMinikube(ctx context.Context, command []string) (string, error) { + cmd := exec.CommandContext(ctx, command[0], command[1:]...) + cmd.Dir = minikubeDir() + cmd.Env = os.Environ() + + buf := bytes.NewBuffer([]byte{}) + cmd.Stdout = buf + + log.Printf("Running: %v", cmd.Args) + if err := cmd.Run(); err != nil { + return "", errors.Wrapf(err, "running %v: %v", cmd.Args, buf.String()) + } + return buf.String(), nil +} + +func minikubeDir() string { + return filepath.Join(os.Getenv("HOME"), "minikube") +} diff --git a/pkg/perf/monitor/github.go b/pkg/perf/monitor/github.go new file mode 100644 index 0000000000..eb3146136e --- /dev/null +++ b/pkg/perf/monitor/github.go @@ -0,0 +1,179 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +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 monitor + +import ( + "context" + "log" + "os" + "time" + + "github.com/google/go-github/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" +) + +// Client provides the context and client with necessary auth +// for interacting with the Github API +type Client struct { + ctx context.Context + *github.Client + owner string + repo string +} + +// NewClient returns a github client with the necessary auth +func NewClient(ctx context.Context, owner, repo string) *Client { + githubToken := os.Getenv(GithubAccessTokenEnvVar) + // Setup the token for github authentication + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: githubToken}, + ) + tc := oauth2.NewClient(context.Background(), ts) + + // Return a client instance from github + client := github.NewClient(tc) + return &Client{ + ctx: ctx, + Client: client, + owner: owner, + repo: repo, + } +} + +// CommentOnPR comments message on the PR +func (g *Client) CommentOnPR(pr int, message string) error { + comment := &github.IssueComment{ + Body: &message, + } + + log.Printf("Creating comment on PR %d: %s", pr, message) + _, _, err := g.Client.Issues.CreateComment(g.ctx, g.owner, g.repo, pr, comment) + if err != nil { + return errors.Wrap(err, "creating github comment") + } + log.Printf("Successfully commented on PR %d.", pr) + return nil +} + +// ListOpenPRsWithLabel returns all open PRs with the specified label +func (g *Client) ListOpenPRsWithLabel(label string) ([]int, error) { + validPrs := []int{} + prs, _, err := g.Client.PullRequests.List(g.ctx, g.owner, g.repo, &github.PullRequestListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "listing pull requests") + } + for _, pr := range prs { + if prContainsLabel(pr.Labels, label) { + validPrs = append(validPrs, pr.GetNumber()) + } + } + return validPrs, nil +} + +func prContainsLabel(labels []*github.Label, label string) bool { + for _, l := range labels { + if l == nil { + continue + } + if l.GetName() == label { + return true + } + } + return false +} + +// NewCommitsExist checks if new commits exist since minikube-pr-bot +// commented on the PR. If so, return true. +func (g *Client) NewCommitsExist(pr int, login string) (bool, error) { + lastCommentTime, err := g.timeOfLastComment(pr, login) + if err != nil { + return false, errors.Wrapf(err, "getting time of last comment by %s on pr %d", login, pr) + } + lastCommitTime, err := g.timeOfLastCommit(pr) + if err != nil { + return false, errors.Wrapf(err, "getting time of last commit on pr %d", pr) + } + return lastCommentTime.Before(lastCommitTime), nil +} + +func (g *Client) timeOfLastCommit(pr int) (time.Time, error) { + var commits []*github.RepositoryCommit + + page := 0 + resultsPerPage := 30 + for { + c, _, err := g.Client.PullRequests.ListCommits(g.ctx, g.owner, g.repo, pr, &github.ListOptions{ + Page: page, + PerPage: resultsPerPage, + }) + if err != nil { + return time.Time{}, err + } + commits = append(commits, c...) + if len(c) < resultsPerPage { + break + } + page++ + } + + lastCommitTime := time.Time{} + for _, c := range commits { + if newCommitTime := c.GetCommit().GetAuthor().GetDate(); newCommitTime.After(lastCommitTime) { + lastCommitTime = newCommitTime + } + } + return lastCommitTime, nil +} + +func (g *Client) timeOfLastComment(pr int, login string) (time.Time, error) { + var comments []*github.IssueComment + + page := 0 + resultsPerPage := 30 + for { + c, _, err := g.Client.Issues.ListComments(g.ctx, g.owner, g.repo, pr, &github.IssueListCommentsOptions{ + ListOptions: github.ListOptions{ + Page: page, + PerPage: resultsPerPage, + }, + }) + if err != nil { + return time.Time{}, err + } + comments = append(comments, c...) + if len(c) < resultsPerPage { + break + } + page++ + } + + // go through comments backwards to find the most recent + lastCommentTime := time.Time{} + + for _, c := range comments { + if u := c.GetUser(); u != nil { + if u.GetLogin() == login { + if c.GetCreatedAt().After(lastCommentTime) { + lastCommentTime = c.GetCreatedAt() + } + } + } + } + + return lastCommentTime, nil +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 6a5c3bf0f6..518c1d7966 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -24,7 +24,7 @@ import ( "strconv" "github.com/blang/semver" - "github.com/docker/go-units" + units "github.com/docker/go-units" "github.com/pkg/errors" ) @@ -47,6 +47,18 @@ func CalculateSizeInMB(humanReadableSize string) (int, error) { return int(size / units.MiB), nil } +func ConvertMBToBytes(mbSize int) int64 { + return int64(mbSize) * units.MiB +} + +func ConvertBytesToMB(byteSize int64) int { + return int(ConvertUnsignedBytesToMB(uint64(byteSize))) +} + +func ConvertUnsignedBytesToMB(byteSize uint64) int64 { + return int64(byteSize / units.MiB) +} + // GetBinaryDownloadURL returns a suitable URL for the platform func GetBinaryDownloadURL(version, platform string) string { switch platform {