[x/go.dev] cmd/versionprune: add command to prune appengine versions

This change adds a command to prune old app engine versions. AppEngine
has a limit of 210 versions across all services. As we have many
services in this project and deploy go-dev and learn-go-dev daily, we
should clean up services regularly.

There's no gcloud command to do something similar, and doing it in the
UI is tedious and error prone.

The command by default will:
- keep the latest 5 versions
- keep any version that is serving traffic
- keep any version that is younger than 24h
- ignore versions with invalid dates (doesn't seem to be a real thing)

Updates b/143768957

Change-Id: I315ca9f459aa28b8ae94b55557e628ceeb22c884
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go.dev/+/589755
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
X-GoDev-Commit: b763768db4b9b9b2fcd67a3f5a0f3d18fe65cd9e
This commit is contained in:
Alexander Rakoczy 2019-10-31 19:44:27 -04:00
Родитель 87964e9137
Коммит 689fe959ee
5 изменённых файлов: 419 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,42 @@
/*
Binary versionprune prunes stale AppEngine versions for a specified service.
The command by default will:
- keep the latest 5 versions
- keep any version that is serving traffic
- keep any version that is younger than 24h
- ignore versions with invalid dates (doesn't seem to be a real thing)
Sample output:
target project: [go-discovery]
target service: [go-dev]
versions: (18)
versions to keep (11): [
20191101t013408: version is serving traffic. split: 100%
20191031t211924: keeping the latest 5 versions (2)
20191031t211903: keeping the latest 5 versions (3)
20191031t205920: keeping the latest 5 versions (4)
20191031t232247: keeping the latest 5 versions (5)
20191031t232028: keeping recent versions (2h56m4.73591512s)
20191031t220312: keeping recent versions (4h13m20.735921508s)
20191031t211935: keeping recent versions (4h56m55.73592447s)
20191031t211824: keeping recent versions (4h58m10.735928067s)
20191031t200353: keeping recent versions (6h12m38.735932792s)
20191031t150644: keeping recent versions (11h9m44.735935312s)
]
versions to delete (7): [
20191030t225128: bye
20191030t214823: bye
20191030t214355: bye
20191030t204338: bye
20191030t202841: bye
20191030t195403: bye
20191030t192250: bye
]
deleting go-discovery/go-dev/20191030t225128
...
*/
package main

Просмотреть файл

@ -0,0 +1,175 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"sort"
"time"
"google.golang.org/api/appengine/v1"
)
var (
dryRun = flag.Bool("dry_run", true, "When true, just print intended modifications and quit")
keepDuration = flag.Duration("keep_duration", 24*time.Hour, "Versions older than this will be deleted")
keepNumber = flag.Int("keep_number", 5, "Minimum number of versions to keep")
project = flag.String("project", "", "GCP Project (required)")
service = flag.String("service", "", "AppEngine service (required)")
)
func main() {
flag.Parse()
if *project == "" {
fmt.Println("-project flag is required.")
flag.Usage()
os.Exit(1)
}
if *service == "" {
fmt.Println("-service flag is required.")
flag.Usage()
os.Exit(1)
}
if *keepDuration < 0 {
fmt.Printf("-keep_duration must be greater or equal to 0, got %s\n", *keepDuration)
flag.Usage()
os.Exit(1)
}
if *keepNumber < 0 {
fmt.Printf("-keep_number must be greater or equal to 0, got %d\n", *keepNumber)
flag.Usage()
os.Exit(1)
}
if err := run(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
// version is an intermediate representation of an AppEngine version.
type version struct {
AEVersion *appengine.Version
// Message is a human-readable message of why a version was bucketed.
Message string
// Created is a parsed time of the AppEngine Version CreateTime.
Created time.Time
}
// run fetches, processes, and (if DryRun is false) deletes stale AppEngine versions for the specified Service.
func run(ctx context.Context) error {
aes, err := appengine.NewService(ctx)
if err != nil {
return fmt.Errorf("creating appengine client: %w", err)
}
ass := appengine.NewAppsServicesService(aes)
asvs := appengine.NewAppsServicesVersionsService(aes)
s, err := ass.Get(*project, *service).Do()
if err != nil {
return fmt.Errorf("fetching service: %w", err)
}
vs, err := getVersions(ctx, asvs)
if err != nil {
return fmt.Errorf("fetching versions: %w", err)
}
bs, err := bucket(s.Split.Allocations, vs, *keepNumber, *keepDuration)
if err != nil {
return fmt.Errorf("bucketing versions: %w", err)
}
printIntent(bs)
if err := act(asvs, bs, *dryRun); err != nil {
return fmt.Errorf("executing: %w", err)
}
return nil
}
func getVersions(ctx context.Context, asvs *appengine.AppsServicesVersionsService) ([]*appengine.Version, error) {
var versions []*appengine.Version
err := asvs.List(*project, *service).Pages(ctx, func(r *appengine.ListVersionsResponse) error {
versions = append(versions, r.Versions...)
return nil
})
if err != nil {
return nil, err
}
sort.Slice(versions, func(i, j int) bool {
// Sort by create time, descending.
return versions[i].CreateTime > versions[j].CreateTime
})
return versions, nil
}
type buckets struct {
keep []version
delete []version
}
// bucket splits c.versions into intended actions.
func bucket(allocs map[string]float64, versions []*appengine.Version, keepNumber int, keepDuration time.Duration) (buckets, error) {
var bs buckets
for _, av := range versions {
v := version{AEVersion: av}
created, err := time.Parse(time.RFC3339, av.CreateTime)
if err != nil {
return bs, fmt.Errorf("failed to parse time %q for version %s: %v", av.CreateTime, av.Id, err)
}
v.Created = created
if s, ok := allocs[av.Id]; ok {
v.Message = fmt.Sprintf("version is serving traffic. split: %v%%", s*100)
bs.keep = append(bs.keep, v)
continue
}
if len(bs.keep) < keepNumber {
v.Message = fmt.Sprintf("keeping the latest %d versions (%d)", keepNumber, len(bs.keep))
bs.keep = append(bs.keep, v)
continue
}
if dur := time.Since(v.Created); dur < keepDuration {
v.Message = fmt.Sprintf("keeping recent versions (%s)", dur)
bs.keep = append(bs.keep, v)
continue
}
v.Message = "bye"
bs.delete = append(bs.delete, v)
}
return bs, nil
}
func printIntent(bs buckets) {
fmt.Printf("target project:\t[%v]\n", *project)
fmt.Printf("target service:\t[%v]\n", *service)
fmt.Printf("versions:\t(%v)\n", len(bs.delete)+len(bs.keep))
fmt.Println()
fmt.Printf("versions to keep (%v): [\n", len(bs.keep))
for _, v := range bs.keep {
fmt.Printf("\t%v: %v\n", v.AEVersion.Id, v.Message)
}
fmt.Println("]")
fmt.Printf("versions to delete (%v): [\n", len(bs.delete))
for _, v := range bs.delete {
fmt.Printf("\t%v: %v\n", v.AEVersion.Id, v.Message)
}
fmt.Println("]")
}
// act performs delete requests for AppEngine services to be deleted. No deletions are performed if DryRun is true.
func act(asvs *appengine.AppsServicesVersionsService, bs buckets, dryRun bool) error {
for _, v := range bs.delete {
if dryRun {
fmt.Printf("dry-run: skipping delete %v: %v\n", v.AEVersion.Id, v.Message)
continue
}
fmt.Printf("deleting %v/%v/%v\n", *project, *service, v.AEVersion.Id)
if _, err := asvs.Delete(*project, *service, v.AEVersion.Id).Do(); err != nil {
return fmt.Errorf("error deleting %v/%v/%v: %w", *project, *service, v.AEVersion.Id, err)
}
}
return nil
}

Просмотреть файл

@ -0,0 +1,118 @@
package main
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/api/appengine/v1"
)
func TestDecide(t *testing.T) {
recentTime := time.Now()
recentTime = recentTime.Truncate(time.Minute)
allocs := map[string]float64{"currentlyServingID": 1.0}
tests := []struct {
desc string
versions []*appengine.Version
keepNumber int
wantKeep []version
wantDelete []version
wantErr bool
}{
{
desc: "no versions",
},
{
desc: "invalid Version time",
versions: []*appengine.Version{{Id: "invalid time", CreateTime: "abc123"}},
wantErr: true,
},
{
desc: "old versions",
versions: []*appengine.Version{{Id: "old one", CreateTime: "2018-01-02T15:04:05Z"}},
wantDelete: []version{{
AEVersion: &appengine.Version{Id: "old one", CreateTime: "2018-01-02T15:04:05Z"},
Created: time.Date(2018, 1, 2, 15, 4, 5, 0, time.UTC),
}},
},
{
desc: "versions serving",
versions: []*appengine.Version{{Id: "currentlyServingID", CreateTime: "2018-01-02T15:04:05Z"}},
wantKeep: []version{{
AEVersion: &appengine.Version{Id: "currentlyServingID", CreateTime: "2018-01-02T15:04:05Z"},
Created: time.Date(2018, 1, 2, 15, 4, 5, 0, time.UTC),
}},
},
{
desc: "within 24h",
versions: []*appengine.Version{{Id: "some id", CreateTime: recentTime.Format(time.RFC3339)}},
wantKeep: []version{{
AEVersion: &appengine.Version{Id: "some id", CreateTime: recentTime.Format(time.RFC3339)},
Created: recentTime,
}},
},
{
desc: "keeps KeepNumber versions",
versions: []*appengine.Version{
{Id: "some id", CreateTime: recentTime.Format(time.RFC3339)},
{Id: "currentlyServingID", CreateTime: "2018-01-02T15:04:05Z"},
{Id: "not serving", CreateTime: "2019-01-02T15:04:05Z"},
{Id: "this one should be deleted", CreateTime: "2019-01-02T15:04:05Z"},
},
keepNumber: 3,
wantKeep: []version{
{
AEVersion: &appengine.Version{Id: "some id", CreateTime: recentTime.Format(time.RFC3339)},
Created: recentTime,
},
{
AEVersion: &appengine.Version{Id: "currentlyServingID", CreateTime: "2018-01-02T15:04:05Z"},
Created: time.Date(2018, 1, 2, 15, 4, 5, 0, time.UTC),
},
{
AEVersion: &appengine.Version{Id: "not serving", CreateTime: "2019-01-02T15:04:05Z"},
Created: time.Date(2019, 1, 2, 15, 4, 5, 0, time.UTC),
},
},
wantDelete: []version{{
AEVersion: &appengine.Version{Id: "this one should be deleted", CreateTime: "2019-01-02T15:04:05Z"},
Created: time.Date(2019, 1, 2, 15, 4, 5, 0, time.UTC),
}},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
bs, err := bucket(allocs, tt.versions, tt.keepNumber, 24*time.Hour)
if (err != nil) != tt.wantErr {
t.Errorf("bucket(%v, %v, %v, %v) = %v, %v, wantErr %v", allocs, tt.versions, tt.keepNumber, 24*time.Hour, bs, err, tt.wantErr)
return
}
ignoreFields := cmpopts.IgnoreFields(version{}, "Message")
if diff := cmp.Diff(tt.wantKeep, bs.keep, ignoreFields); diff != "" {
t.Errorf("c.Keep mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(tt.wantDelete, bs.delete, ignoreFields); diff != "" {
t.Errorf("c.Delete mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestAct(t *testing.T) {
bs := buckets{delete: []version{{AEVersion: &appengine.Version{Id: "test ID"}}}}
if err := act(nil, bs, true); err != nil {
t.Errorf("c.act() = %v, wanted no error", err)
}
defer func(t *testing.T) {
t.Helper()
if recover() == nil {
// c.act() should panic with no asvs set, showing that the DryRun flag worked.
// faking out the appengine admin client is hard.
t.Errorf("recover() = nil, wanted panic")
}
}(t)
act(nil, bs, false)
}

Просмотреть файл

@ -5,5 +5,6 @@ go 1.13
require (
github.com/google/go-cmp v0.3.1
github.com/microcosm-cc/bluemonday v1.0.2
google.golang.org/api v0.13.0
gopkg.in/yaml.v2 v2.2.2
)

Просмотреть файл

@ -1,10 +1,93 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=