From 1869285de4f5c32994c8a63c7b031abbfe6ab77e Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 8 Dec 2019 19:13:36 +0000 Subject: [PATCH] query logs --- cmd/wasmcloud-executor/main.go | 30 +++++-- cmd/wasmcloud/handler-create.go | 96 +++++++++++++++++++++ cmd/wasmcloud/login.go | 4 +- cmd/wasmcloud/main.go | 2 +- cmd/wasmcloud/test_work.go | 82 ------------------ cmd/wasmcloudd/handler.go | 148 +++++++++++++++++++++++++++++++- cmd/wasmcloudd/main.go | 19 +++- cmd/wasmcloudd/models.go | 11 ++- go.mod | 3 + go.sum | 18 ++++ 10 files changed, 317 insertions(+), 96 deletions(-) create mode 100644 cmd/wasmcloud/handler-create.go delete mode 100644 cmd/wasmcloud/test_work.go diff --git a/cmd/wasmcloud-executor/main.go b/cmd/wasmcloud-executor/main.go index ac0e2d4..5e9d7ed 100644 --- a/cmd/wasmcloud-executor/main.go +++ b/cmd/wasmcloud-executor/main.go @@ -14,7 +14,9 @@ import ( "syscall" "time" + "github.com/facebookgo/flagenv" shell "github.com/ipfs/go-ipfs-api" + nats "github.com/nats-io/nats.go" "github.com/perlin-network/life/exec" "github.com/rogpeppe/go-internal/txtar" "tulpa.dev/within/wasmcloud/cmd/internal" @@ -22,20 +24,26 @@ import ( ) var ( - ipfsURL = flag.String("ipfs-host", "localhost:5001", "IPFS host (must have pubsub experiment enabled)") + natsURL = flag.String("nats-url", nats.DefaultURL, "nats URL") + ipfsURL = flag.String("ipfs-host", "localhost:5001", "IPFS host") workerCount = flag.Int("worker-count", 1, "number of wasm executor workers") gasLimit = flag.Int("gas-limit", 1048576, "number of wasm instructions per execution") ramLimit = flag.Int("ram-limit", 128, "number of wasm pages that can be used") + loopTimeout = flag.Duration("loop-timeout", 30*time.Second, "idle time per loop") ) func main() { + flagenv.Parse() + flag.Parse() log.SetFlags(log.LstdFlags | log.Lshortfile) sh := shell.NewShell(*ipfsURL) - subsc, err := sh.PubSubSubscribe(internal.TopicName) + + nc, err := nats.Connect(*natsURL) if err != nil { log.Fatal(err) } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -53,15 +61,19 @@ func main() { wg.Add(*workerCount) for range make([]struct{}, *workerCount) { - go waitForNewWASM(ctx, &wg, sh, subsc) + go waitForNewWASM(ctx, &wg, sh, nc) } log.Printf("waiting for work on %s", internal.TopicName) wg.Wait() } -func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, subsc *shell.PubSubSubscription) { +func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, nc *nats.Conn) { defer wg.Done() + subsc, err := nc.QueueSubscribeSync(internal.TopicName, "workers") + if err != nil { + return + } for { select { @@ -70,8 +82,12 @@ func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, su default: } - msg, err := subsc.Next() + msg, err := subsc.NextMsg(*loopTimeout) if err != nil { + if err == nats.ErrTimeout { + continue + } + log.Printf("error getting message: %v", err) return } @@ -125,7 +141,7 @@ func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, su } arc := txtar.Archive{ - Comment: []byte(fmt.Sprintf("%s: execution of %s at %s", er.UUID, er.WASMCID, result.StartTime.Format(time.RFC3339))), + Comment: []byte(fmt.Sprintf("%s: execution of %s (%s) at %s", er.UUID, er.Name, er.WASMCID, result.StartTime.Format(time.RFC3339))), Files: []txtar.File{ { Name: "logs.txt", @@ -162,6 +178,6 @@ func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, su UUID: er.UUID, } repMsg, _ := json.Marshal(resp) - sh.PubSubPublish(er.UUID, string(repMsg)) + msg.Respond(repMsg) } } diff --git a/cmd/wasmcloud/handler-create.go b/cmd/wasmcloud/handler-create.go new file mode 100644 index 0000000..509c370 --- /dev/null +++ b/cmd/wasmcloud/handler-create.go @@ -0,0 +1,96 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "github.com/google/subcommands" + "tulpa.dev/within/wasmcloud/cmd/internal" +) + +type handlerCreateCmd struct { + abi string +} + +func (handlerCreateCmd) Name() string { return "create" } +func (handlerCreateCmd) Synopsis() string { return "create a new handler" } +func (handlerCreateCmd) Usage() string { + return `wasmcloud create [options] + +$ wasmcloud create filename.wasm +$ wasmcloud create -abi dagger filename.wasm + +Creates a new handler on wasmcloud. Returns a name you can use to invoke the +function. + +Flags: +` +} + +func (h *handlerCreateCmd) SetFlags(fs *flag.FlagSet) { + fs.StringVar(&h.abi, "abi", "cwa", "WebAssembly ABI to use for the handler") +} + +func (h handlerCreateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if fs.NArg() != 1 { + fmt.Println("usage: wasmcloud create [options] ") + return subcommands.ExitUsageError + } + + fname := fs.Arg(0) + data, err := ioutil.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + + hdlr := internal.Handler{ + ABI: h.abi, + WASM: data, + } + + bodyData, _ := json.Marshal(hdlr) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, *apiServer+"/api/handler/create", bytes.NewBuffer(bodyData)) + if err != nil { + log.Fatal(err) + } + withAPI(req) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + + if resp.StatusCode != http.StatusOK { + io.Copy(os.Stdout, resp.Body) + return subcommands.ExitFailure + } + + type apiResp struct { + ID int `json:"ID"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + Name string `json:"Name"` + Path string `json:"Path"` + } + + var result apiResp + err = json.NewDecoder(resp.Body).Decode(&result) + resp.Body.Close() + if err != nil { + log.Fatal(err) + } + + fmt.Println("created new handler") + fmt.Printf("name: %s\nIPFS ID: %s\n", result.Name, result.Path) + + return subcommands.ExitSuccess +} diff --git a/cmd/wasmcloud/login.go b/cmd/wasmcloud/login.go index ca6bfd3..adadc2b 100644 --- a/cmd/wasmcloud/login.go +++ b/cmd/wasmcloud/login.go @@ -35,11 +35,13 @@ func (l *loginCmd) SetFlags(fs *flag.FlagSet) { fs.StringVar(&l.username, "username", "", "wasmcloud username") } -func (l *loginCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { +func init() { http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } +} +func (l *loginCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { cfg, err := loadConfig() if err != nil { log.Printf("error loading config: %v", err) diff --git a/cmd/wasmcloud/main.go b/cmd/wasmcloud/main.go index a55dfdc..881b954 100644 --- a/cmd/wasmcloud/main.go +++ b/cmd/wasmcloud/main.go @@ -20,8 +20,8 @@ func main() { subcommands.Register(subcommands.CommandsCommand(), "") subcommands.Register(&loginCmd{}, "api") subcommands.Register(&whoamiCmd{}, "api") + subcommands.Register(&handlerCreateCmd{}, "handlers") subcommands.Register(namegenCmd{}, "utils") - subcommands.Register(&testWorkCommand{}, "utils") subcommands.Register(&runCmd{}, "utils") subcommands.ImportantFlag("api-server") subcommands.ImportantFlag("config") diff --git a/cmd/wasmcloud/test_work.go b/cmd/wasmcloud/test_work.go deleted file mode 100644 index b1eeb9d..0000000 --- a/cmd/wasmcloud/test_work.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - - "github.com/google/subcommands" - "github.com/google/uuid" - shell "github.com/ipfs/go-ipfs-api" - "github.com/rogpeppe/go-internal/txtar" - "tulpa.dev/within/wasmcloud/cmd/internal" -) - -type testWorkCommand struct { - wasmCID, abi, ipfsURL string -} - -func (testWorkCommand) Name() string { return "test-work" } -func (testWorkCommand) Synopsis() string { return "sends a test work message over IPFS" } -func (testWorkCommand) Usage() string { return "TODO(within): this" } - -func (t *testWorkCommand) SetFlags(fs *flag.FlagSet) { - fs.StringVar(&t.wasmCID, "wasm-cid", "", "CID for the WASM to run") - fs.StringVar(&t.abi, "abi", "cwa", "which ABI to use") - fs.StringVar(&t.ipfsURL, "ipfs-host", "127.0.0.1:5001", "IPFS host") -} - -func (t testWorkCommand) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - sh := shell.NewShell(t.ipfsURL) - rdr, err := sh.Cat(t.wasmCID) - if err != nil { - log.Fatalf("error getting %s: %v", t.wasmCID, err) - } - io.Copy(ioutil.Discard, rdr) - rdr.Close() - - er := internal.ExecRequest{ - WASMCID: t.wasmCID, - ABI: internal.ABI(t.abi), - Data: []byte("hello, world"), - Env: map[string]string{ - "MAGIC_CONCH": "yes", - }, - UUID: uuid.New().String(), - } - - data, err := json.Marshal(er) - if err != nil { - panic(err) - } - - err = sh.PubSubPublish(internal.TopicName, string(data)) - if err != nil { - log.Printf("can't publish data: %v", err) - return subcommands.ExitFailure - } - - subsc, err := sh.PubSubSubscribe(er.UUID) - if err != nil { - log.Fatal(err) - } - - msg, err := subsc.Next() - if err != nil { - log.Fatal(err) - } - - var execResp internal.ExecResponse - err = json.Unmarshal(msg.Data, &execResp) - if err != nil { - log.Fatal(err) - } - - fmt.Println(string(txtar.Format(&execResp.Logs))) - - return subcommands.ExitSuccess -} diff --git a/cmd/wasmcloudd/handler.go b/cmd/wasmcloudd/handler.go index 090116b..09165d4 100644 --- a/cmd/wasmcloudd/handler.go +++ b/cmd/wasmcloudd/handler.go @@ -5,8 +5,12 @@ import ( "encoding/json" "io/ioutil" "net/http" + "time" + "github.com/go-interpreter/wagon/wasm" + "github.com/google/uuid" shell "github.com/ipfs/go-ipfs-api" + "github.com/rogpeppe/go-internal/txtar" "tulpa.dev/within/wasmcloud/cmd/internal" "within.website/ln" "within.website/olin/namegen" @@ -20,7 +24,7 @@ func createHandler(w http.ResponseWriter, r *http.Request, u *User) { ctx := r.Context() sh := shell.NewShell(*ipfsURL) - data, err := ioutil.ReadAll(r.Body) + data, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, 16*1024*1024)) if err != nil { ln.Error(ctx, err) http.Error(w, "invalid data", http.StatusBadRequest) @@ -44,6 +48,13 @@ func createHandler(w http.ResponseWriter, r *http.Request, u *User) { return } + buf := bytes.NewBuffer(hdlr.WASM) + _, err = wasm.DecodeModule(buf) + if err != nil { + ln.Error(ctx, err) + http.Error(w, "not webassembly", http.StatusBadRequest) + } + cid, err := sh.Add(bytes.NewBuffer(hdlr.WASM)) if err != nil { ln.Error(ctx, err) @@ -67,3 +78,138 @@ func createHandler(w http.ResponseWriter, r *http.Request, u *User) { json.NewEncoder(w).Encode(h) } + +func listHandlers(w http.ResponseWriter, r *http.Request, u *User) { + ctx := r.Context() + var hdlrs []Handler + + err := db.Where("user_id = ?", u.ID).Scan(&hdlrs).Error + if err != nil { + ln.Error(ctx, err) + http.Error(w, "can't read handlers", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(hdlrs) +} + +func getLogs(w http.ResponseWriter, r *http.Request, u *User) { + ctx := r.Context() + q := r.URL.Query() + name := q.Get("name") + if name == "" { + http.NotFound(w, r) + return + } + + var hdlr Handler + err := db.Where("name = ? AND user_id = ?", name, u.ID).First(&hdlr).Error + if err != nil { + ln.Error(ctx, err) + http.NotFound(w, r) + return + } + + var elogs []ExecutionLog + err = db.Where("handler_id = ?", hdlr.ID).Find(&elogs).Error + if err != nil { + ln.Error(ctx, err) + http.Error(w, "handler id not found", http.StatusInternalServerError) + return + } + + type resultLine struct { + ExecID string `json:"exec_id"` + Comment string `json:"comment"` + Logs map[string]string `json:"logs"` + } + var result []resultLine + + for _, elog := range elogs { + arc := txtar.Parse(elog.Data) + logs := map[string]string{} + + for _, file := range arc.Files { + logs[file.Name] = string(file.Data) + } + + result = append(result, resultLine{ + ExecID: elog.RunID, + Comment: string(arc.Comment), + Logs: logs, + }) + } + + json.NewEncoder(w).Encode(result) +} + +func invokeHandler(w http.ResponseWriter, r *http.Request, u *User) { + ctx := r.Context() + q := r.URL.Query() + name := q.Get("name") + if name == "" { + http.NotFound(w, r) + return + } + + var hdlr Handler + err := db.Where("name = ?", name).First(&hdlr).Error + if err != nil { + ln.Error(ctx, err) + http.NotFound(w, r) + return + } + + data, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, 1*1024*1024)) + if err != nil { + ln.Error(ctx, err) + http.NotFound(w, r) + } + + execID := uuid.New().String() + + er := internal.ExecRequest{ + WASMCID: hdlr.Path, + Name: hdlr.Name, + Data: data, + Env: map[string]string{ + "RUN_ID": execID, + }, + UUID: execID, + } + + encData, err := json.Marshal(er) + if err != nil { + ln.Error(ctx, err) + http.NotFound(w, r) + } + + go func() { + msg, err := nc.Request(internal.TopicName, encData, 5*time.Minute) + if err != nil { + ln.Error(ctx, err) + return + } + + var resp internal.ExecResponse + err = json.Unmarshal(msg.Data, &resp) + if err != nil { + ln.Error(ctx, err) + return + } + + data := txtar.Format(&resp.Logs) + + entry := ExecutionLog{ + HandlerID: hdlr.ID, + RunID: execID, + Data: data, + } + + err = db.Save(&entry).Error + if err != nil { + ln.Error(ctx, err) + return + } + }() +} diff --git a/cmd/wasmcloudd/main.go b/cmd/wasmcloudd/main.go index 7d036c5..0bdcb87 100644 --- a/cmd/wasmcloudd/main.go +++ b/cmd/wasmcloudd/main.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/jinzhu/gorm" _ "github.com/mattn/go-sqlite3" + nats "github.com/nats-io/nats.go" ) var ( @@ -18,8 +19,10 @@ var ( databaseKind = flag.String("database-kind", "sqlite3", "database kind") defaultTokenLifetime = flag.Duration("default-token-lifetime", 500*24*time.Hour, "default token lifetime") ipfsURL = flag.String("ipfs-host", "127.0.0.1:5001", "URL of the IPFS API") + natsURL = flag.String("nats-url", nats.DefaultURL, "URL of the nats server") db *gorm.DB + nc *nats.Conn ) func main() { @@ -32,9 +35,15 @@ func main() { } log.Println("migrating") - gormDB.AutoMigrate(User{}, Token{}, Handler{}) + gormDB.AutoMigrate(User{}, Token{}, Handler{}, ExecutionLog{}) db = gormDB + natsClient, err := nats.Connect(*natsURL) + if err != nil { + log.Fatal(err) + } + nc = natsClient + rtr := mux.NewRouter() // auth @@ -46,8 +55,14 @@ func main() { rtr.HandleFunc("/", unauthenticatedShowAPage("index")) rtr.HandleFunc("/control/", authenticatedShowAPage("controlindex")) - rtr.HandleFunc("/api/whoami", makeHandler(true, apiWhoami)) + // API routes + rtr.HandleFunc("/api/handler", makeHandler(true, listHandlers)).Methods(http.MethodGet) rtr.HandleFunc("/api/handler/create", makeHandler(true, createHandler)).Methods(http.MethodPost) + rtr.HandleFunc("/api/handler/logs", makeHandler(true, getLogs)).Methods(http.MethodGet) + rtr.HandleFunc("/api/whoami", makeHandler(true, apiWhoami)) + + // invocation + rtr.HandleFunc("/invoke", makeHandler(false, invokeHandler)).Methods(http.MethodPost) rtr.PathPrefix("/static/").Handler(http.FileServer(http.Dir("."))) diff --git a/cmd/wasmcloudd/models.go b/cmd/wasmcloudd/models.go index b08384b..a2fb1d1 100644 --- a/cmd/wasmcloudd/models.go +++ b/cmd/wasmcloudd/models.go @@ -26,10 +26,17 @@ type Token struct { ExpiresAt time.Time } +type ExecutionLog struct { + gorm.Model + RunID string `gorm:"unique;not null"` + HandlerID uint + Data []byte `gorm:"not null"` +} + type Handler struct { gorm.Model - Name string - Path string + Name string `gorm:"unique;not null"` + Path string `gorm:"unique;not null"` UserID uint User User } diff --git a/go.mod b/go.mod index 12377d3..143ca0c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ replace github.com/go-interpreter/wagon v0.0.0 => github.com/perlin-network/wago require ( github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 github.com/fatih/color v1.7.0 // indirect + github.com/go-interpreter/wagon v0.0.0 github.com/google/subcommands v1.0.1 github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.3 @@ -15,6 +16,8 @@ require ( github.com/jinzhu/gorm v1.9.11 github.com/manifoldco/promptui v0.6.0 github.com/mattn/go-sqlite3 v2.0.1+incompatible + github.com/nats-io/nats-server/v2 v2.1.2 // indirect + github.com/nats-io/nats.go v1.9.1 github.com/perlin-network/life v0.0.0-20190204091834-d05763d11050 github.com/rogpeppe/go-internal v1.5.0 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 diff --git a/go.sum b/go.sum index 4e019c7..b611d18 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb 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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -230,6 +231,21 @@ github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d github.com/multiformats/go-multihash v0.0.1 h1:HHwN1K12I+XllBCrqKnhX949Orn4oawPkegHMu2vDqQ= github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0 h1:xdnzwFETV++jNc4W1mw//qFyJGb2ABOombmZJQS4+Qo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server v1.4.1 h1:Ul1oSOGNV/L8kjr4v6l2f9Yet6WY+LevH1/7cRZ/qyA= +github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -324,6 +340,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -370,6 +387,7 @@ golang.org/x/sys v0.0.0-20190219203350-90b0e4468f99/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=