support policies, closes #12

This commit is contained in:
Cadey Ratio 2019-12-14 21:04:28 +00:00
parent 37093adeab
commit 7e44d75f68
14 changed files with 166 additions and 17 deletions

18
cmd/internal/policy.go Normal file
View File

@ -0,0 +1,18 @@
package internal
import (
"fmt"
"github.com/hashicorp/go-multierror"
"within.website/olin/policy"
)
func ValidatePolicy(pol policy.Policy) error {
var err error
if pol.RamPageLimit > 2048 {
err = multierror.Append(err, fmt.Errorf("ram page limit is %d, which is over the limit of 2048", pol.RamPageLimit))
}
return err
}

View File

@ -3,12 +3,13 @@ package internal
import "github.com/rogpeppe/go-internal/txtar" import "github.com/rogpeppe/go-internal/txtar"
// Topic name for wasmcloud -> executor communication // Topic name for wasmcloud -> executor communication
const TopicName = "wasmcloud-to-executor-09d7e475-71ac-4bdd-be37-050af7a81c58" const TopicName = "exec"
type ExecRequest struct { type ExecRequest struct {
WASMCID string `json:"wasmcid"` WASMCID string `json:"wasmcid"`
Name string `json:"name"` Name string `json:"name"`
Data []byte `json:"data"` Data []byte `json:"data"`
Policy []byte `json:"policy"`
ABI ABI `json:"abi"` ABI ABI `json:"abi"`
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
UUID string `json:"uuid"` UUID string `json:"uuid"`

View File

@ -9,5 +9,6 @@ const (
type Handler struct { type Handler struct {
WASM []byte `json:"wasm"` WASM []byte `json:"wasm"`
Policy []byte `json:"policy"`
ABI string `json:"abi"` ABI string `json:"abi"`
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/rogpeppe/go-internal/txtar" "github.com/rogpeppe/go-internal/txtar"
"tulpa.dev/within/wasmcloud/cmd/internal" "tulpa.dev/within/wasmcloud/cmd/internal"
"tulpa.dev/within/wasmcloud/executor" "tulpa.dev/within/wasmcloud/executor"
"within.website/olin/policy"
) )
var ( var (
@ -136,8 +137,17 @@ func waitForNewWASM(ctx context.Context, wg *sync.WaitGroup, sh *shell.Shell, nc
LogSink: logBuf, LogSink: logBuf,
} }
pol, err := policy.Parse(er.Name+".policy", er.Policy)
if err != nil {
log.Printf("can't get policy: %v", err)
continue
}
c.Policy = &pol
result, err := executor.Run(c) result, err := executor.Run(c)
if err != nil { if err != nil {
fmt.Fprintln(logBuf, "can't run binary:", err)
log.Printf("can't run binary: %v", err) log.Printf("can't run binary: %v", err)
} }

View File

@ -15,10 +15,11 @@ import (
"github.com/google/subcommands" "github.com/google/subcommands"
"tulpa.dev/within/wasmcloud/cmd/internal" "tulpa.dev/within/wasmcloud/cmd/internal"
"within.website/olin/policy"
) )
type handlerCreateCmd struct { type handlerCreateCmd struct {
abi string abi, policyFile string
} }
func (handlerCreateCmd) Name() string { return "create" } func (handlerCreateCmd) Name() string { return "create" }
@ -38,6 +39,7 @@ Flags:
func (h *handlerCreateCmd) SetFlags(fs *flag.FlagSet) { func (h *handlerCreateCmd) SetFlags(fs *flag.FlagSet) {
fs.StringVar(&h.abi, "abi", "cwa", "WebAssembly ABI to use for the handler") fs.StringVar(&h.abi, "abi", "cwa", "WebAssembly ABI to use for the handler")
fs.StringVar(&h.policyFile, "policy", "", "if set, use this policy file for the handler")
} }
func (h handlerCreateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (h handlerCreateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
@ -57,6 +59,25 @@ func (h handlerCreateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...in
WASM: data, WASM: data,
} }
if h.policyFile != "" {
data, err := ioutil.ReadFile(h.policyFile)
if err != nil {
log.Fatal(err)
}
pol, err := policy.Parse(h.policyFile, data)
if err != nil {
log.Fatal(err)
}
err = internal.ValidatePolicy(pol)
if err != nil {
log.Fatal(err)
}
hdlr.Policy = data
}
bodyData, _ := json.Marshal(hdlr) bodyData, _ := json.Marshal(hdlr)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, *apiServer+"/api/handler/create", bytes.NewBuffer(bodyData)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, *apiServer+"/api/handler/create", bytes.NewBuffer(bodyData))
if err != nil { if err != nil {

View File

@ -15,10 +15,11 @@ import (
"github.com/google/subcommands" "github.com/google/subcommands"
"tulpa.dev/within/wasmcloud/cmd/internal" "tulpa.dev/within/wasmcloud/cmd/internal"
"within.website/olin/policy"
) )
type handlerUpdateCmd struct { type handlerUpdateCmd struct {
abi string abi, policyFile string
} }
func (handlerUpdateCmd) Name() string { return "update" } func (handlerUpdateCmd) Name() string { return "update" }
@ -36,6 +37,7 @@ Flags:
func (h *handlerUpdateCmd) SetFlags(fs *flag.FlagSet) { func (h *handlerUpdateCmd) SetFlags(fs *flag.FlagSet) {
fs.StringVar(&h.abi, "abi", "cwa", "WebAssembly ABI to use for the handler") fs.StringVar(&h.abi, "abi", "cwa", "WebAssembly ABI to use for the handler")
fs.StringVar(&h.policyFile, "policy", "", "if set, use this policy file for the handler")
} }
func (h handlerUpdateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (h handlerUpdateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
@ -56,6 +58,25 @@ func (h handlerUpdateCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...in
WASM: data, WASM: data,
} }
if h.policyFile != "" {
data, err := ioutil.ReadFile(h.policyFile)
if err != nil {
log.Fatal(err)
}
pol, err := policy.Parse(h.policyFile, data)
if err != nil {
log.Fatal(err)
}
err = internal.ValidatePolicy(pol)
if err != nil {
log.Fatal(err)
}
hdlr.Policy = data
}
bodyData, _ := json.Marshal(hdlr) bodyData, _ := json.Marshal(hdlr)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, *apiServer+"/api/handler/update?name="+hname, bytes.NewBuffer(bodyData)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, *apiServer+"/api/handler/update?name="+hname, bytes.NewBuffer(bodyData))
if err != nil { if err != nil {

View File

@ -17,12 +17,12 @@ import (
"github.com/perlin-network/life/exec" "github.com/perlin-network/life/exec"
"github.com/rogpeppe/go-internal/txtar" "github.com/rogpeppe/go-internal/txtar"
"tulpa.dev/within/wasmcloud/executor" "tulpa.dev/within/wasmcloud/executor"
"within.website/olin/policy"
) )
type runCmd struct { type runCmd struct {
funcName, abi string funcName, abi string
gasLimit uint64 policyFile string
ramLimit int
} }
func (runCmd) Name() string { return "run" } func (runCmd) Name() string { return "run" }
@ -33,7 +33,6 @@ func (runCmd) Usage() string {
return `wasmcloud run [options] <file.wasm> return `wasmcloud run [options] <file.wasm>
$ wasmcloud run olinfetch.wasm $ wasmcloud run olinfetch.wasm
$ wasmcloud run -abi dagger -func-name dagger_main hello_dagger.wasm
Run a given webassembly binary and return the output. Run a given webassembly binary and return the output.
@ -47,8 +46,7 @@ Flags:
func (r *runCmd) SetFlags(fs *flag.FlagSet) { func (r *runCmd) SetFlags(fs *flag.FlagSet) {
fs.StringVar(&r.funcName, "func-name", "_start", "entrypoint function to call") fs.StringVar(&r.funcName, "func-name", "_start", "entrypoint function to call")
fs.StringVar(&r.abi, "abi", "cwa", "ABI to use") fs.StringVar(&r.abi, "abi", "cwa", "ABI to use")
fs.Uint64Var(&r.gasLimit, "gas-limit", 1048576, "number of wasm instructions per execution") fs.StringVar(&r.policyFile, "policy", "", "if set, the policy file to use")
fs.IntVar(&r.ramLimit, "ram-limit", 128, "number of wasm pages that can be used")
} }
func (r runCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { func (r runCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
@ -86,9 +84,7 @@ func (r runCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{})
var stdin bytes.Buffer var stdin bytes.Buffer
c := executor.Config{ c := executor.Config{
VMConfig: exec.VMConfig{ VMConfig: exec.VMConfig{
GasLimit: r.gasLimit,
ReturnOnGasLimitExceeded: true, ReturnOnGasLimitExceeded: true,
MaxMemoryPages: r.ramLimit,
}, },
Name: filepath.Base(fname), Name: filepath.Base(fname),
@ -101,6 +97,22 @@ func (r runCmd) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{})
LogSink: logBuf, LogSink: logBuf,
} }
if r.policyFile != "" {
data, err := ioutil.ReadFile(r.policyFile)
if err != nil {
log.Fatal(err)
}
pol, err := policy.Parse(r.policyFile, data)
if err != nil {
log.Fatal(err)
}
c.VMConfig.MaxMemoryPages = int(pol.RamPageLimit)
c.VMConfig.GasLimit = uint64(pol.GasLimit)
c.Policy = &pol
}
result, err := executor.Run(c) result, err := executor.Run(c)
if err != nil { if err != nil {
log.Printf("can't run binary: %v", err) log.Printf("can't run binary: %v", err)

View File

@ -16,6 +16,7 @@ import (
"within.website/ln" "within.website/ln"
"within.website/ln/opname" "within.website/ln/opname"
"within.website/olin/namegen" "within.website/olin/namegen"
"within.website/olin/policy"
) )
func deleteHandler(w http.ResponseWriter, r *http.Request, u *User) { func deleteHandler(w http.ResponseWriter, r *http.Request, u *User) {
@ -72,6 +73,15 @@ func updateHandler(w http.ResponseWriter, r *http.Request, u *User) {
return return
} }
if len(uHdlr.Policy) != 0 {
err = validatePolicy(name, uHdlr.Policy)
if err != nil {
ln.Error(ctx, err)
http.Error(w, "policy validation failure", http.StatusBadRequest)
return
}
}
cid, err := uploadHandler(uHdlr) cid, err := uploadHandler(uHdlr)
if err != nil { if err != nil {
ln.Error(ctx, err) ln.Error(ctx, err)
@ -79,6 +89,7 @@ func updateHandler(w http.ResponseWriter, r *http.Request, u *User) {
return return
} }
hdlr.Policy = uHdlr.Policy
hdlr.Path = cid hdlr.Path = cid
if err := db.Save(&hdlr).Error; err != nil { if err := db.Save(&hdlr).Error; err != nil {
ln.Error(ctx, err) ln.Error(ctx, err)
@ -89,6 +100,15 @@ func updateHandler(w http.ResponseWriter, r *http.Request, u *User) {
json.NewEncoder(w).Encode(hdlr) json.NewEncoder(w).Encode(hdlr)
} }
func validatePolicy(name string, data []byte) error {
pol, err := policy.Parse(name+".policy", data)
if err != nil {
return err
}
return internal.ValidatePolicy(pol)
}
func uploadHandler(hdlr internal.Handler) (string, error) { func uploadHandler(hdlr internal.Handler) (string, error) {
sh := shell.NewShell(*ipfsURL) sh := shell.NewShell(*ipfsURL)
@ -135,6 +155,19 @@ func createHandler(w http.ResponseWriter, r *http.Request, u *User) {
return return
} }
n := namegen.Next()
if len(hdlr.Policy) == 0 {
hdlr.Policy = []byte(defaultPolicy)
}
err = validatePolicy(n, hdlr.Policy)
if err != nil {
ln.Error(ctx, err)
http.Error(w, "policy validation failure", http.StatusBadRequest)
return
}
cid, err := uploadHandler(hdlr) cid, err := uploadHandler(hdlr)
if err != nil { if err != nil {
ln.Error(ctx, err) ln.Error(ctx, err)
@ -142,12 +175,12 @@ func createHandler(w http.ResponseWriter, r *http.Request, u *User) {
return return
} }
n := namegen.Next()
h := Handler{ h := Handler{
Name: n, Name: n,
User: *u, User: *u,
UserID: u.ID, UserID: u.ID,
Path: cid, Path: cid,
Policy: hdlr.Policy,
} }
if err := db.Save(&h).Error; err != nil { if err := db.Save(&h).Error; err != nil {

View File

@ -24,6 +24,7 @@ func runHandler(ctx context.Context, hdlr Handler, timeout time.Duration, messag
"MAGIC_CONCH": "yes", "MAGIC_CONCH": "yes",
}, },
UUID: execID, UUID: execID,
Policy: hdlr.Policy,
} }
encData, err := json.Marshal(er) encData, err := json.Marshal(er)

View File

@ -37,6 +37,7 @@ type Handler struct {
gorm.Model gorm.Model
Name string `gorm:"unique;not null"` Name string `gorm:"unique;not null"`
Path string Path string
Policy []byte
UserID uint `json:"-"` UserID uint `json:"-"`
User User `json:"-"` User User `json:"-"`
} }

14
cmd/wasmcloudd/policy.go Normal file
View File

@ -0,0 +1,14 @@
package main
const defaultPolicy = `## The default policy
allow (
^null://$
^random://$
^zero://$
^https://xena.greedo.xeserv.us/files/hello_olin.txt$
)
ram-page-limit 128
gas-limit 1048576
`

View File

@ -15,6 +15,7 @@ import (
"github.com/perlin-network/life/exec" "github.com/perlin-network/life/exec"
"github.com/rogpeppe/go-internal/txtar" "github.com/rogpeppe/go-internal/txtar"
"within.website/olin/abi/cwa" "within.website/olin/abi/cwa"
"within.website/olin/policy"
) )
var ( var (
@ -24,6 +25,7 @@ var (
type Config struct { type Config struct {
exec.VMConfig exec.VMConfig
Policy *policy.Policy
Name string Name string
FuncName string FuncName string
Env map[string]string Env map[string]string
@ -77,7 +79,8 @@ func Run(c Config) (*Result, error) {
p.Stdout = c.Stdout p.Stdout = c.Stdout
p.Stderr = c.Stderr p.Stderr = c.Stderr
p.Logger = log.New(io.MultiWriter(c.LogSink, os.Stdout), c.Name+" ", log.LstdFlags) p.Logger = log.New(io.MultiWriter(c.LogSink, os.Stdout), c.Name+" ", log.LstdFlags)
p.HC.Transport = WasmcloudBrandingTransport(c.Name, p.HC.Transport) p.HC.Transport = WasmcloudBrandingTransport(c.Name, http.DefaultTransport)
p.Policy = c.Policy
gp := &compiler.SimpleGasPolicy{GasPerInstruction: 1} gp := &compiler.SimpleGasPolicy{GasPerInstruction: 1}
vm, err := exec.NewVirtualMachine(c.Binary, c.VMConfig, p, gp) vm, err := exec.NewVirtualMachine(c.Binary, c.VMConfig, p, gp)
@ -92,9 +95,13 @@ func Run(c Config) (*Result, error) {
pagesBefore := len(vm.Memory) / 65536 pagesBefore := len(vm.Memory) / 65536
begin := time.Now() begin := time.Now()
ret, err := vm.Run(main) ret, err := vm.RunWithGasLimit(main, int(c.Policy.GasLimit))
if err != nil { if err != nil {
return nil, err vm.PrintStackTrace()
return &Result{
Status: -2,
StartTime: begin,
}, err
} }
dur := time.Since(begin) dur := time.Since(begin)

3
go.mod
View File

@ -11,8 +11,10 @@ require (
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-multierror v1.0.0
github.com/ipfs/go-ipfs-api v0.0.2 github.com/ipfs/go-ipfs-api v0.0.2
github.com/jinzhu/gorm v1.9.11 github.com/jinzhu/gorm v1.9.11
github.com/kr/pretty v0.1.0
github.com/lib/pq v1.1.1 github.com/lib/pq v1.1.1
github.com/manifoldco/promptui v0.6.0 github.com/manifoldco/promptui v0.6.0
github.com/mattn/go-sqlite3 v2.0.1+incompatible github.com/mattn/go-sqlite3 v2.0.1+incompatible
@ -20,6 +22,7 @@ require (
github.com/nats-io/nats.go v1.9.1 github.com/nats-io/nats.go v1.9.1
github.com/perlin-network/life v0.0.0-20191203030451-05c0e0f7eaea github.com/perlin-network/life v0.0.0-20191203030451-05c0e0f7eaea
github.com/rogpeppe/go-internal v1.5.0 github.com/rogpeppe/go-internal v1.5.0
github.com/rogpeppe/gohack v1.0.2 // indirect
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392
within.website/ln v0.7.0 within.website/ln v0.7.0
within.website/olin v0.4.1-0.20191214133128-bde4927fad6b within.website/olin v0.4.1-0.20191214133128-bde4927fad6b

6
go.sum
View File

@ -143,7 +143,9 @@ github.com/gxed/hashland/keccakpg v0.0.1 h1:wrk3uMNaMxbXiHibbPO4S0ymqJMm41WiudyF
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
github.com/gxed/hashland/murmur3 v0.0.1 h1:SheiaIt0sda5K+8FLz952/1iWS9zrnKsEJaOJu4ZbSc= github.com/gxed/hashland/murmur3 v0.0.1 h1:SheiaIt0sda5K+8FLz952/1iWS9zrnKsEJaOJu4ZbSc=
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48= github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -299,8 +301,11 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw= github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/gohack v1.0.2 h1:lYiGLFzvZC3RvzeE4GoUV3nTecDxTpVusVsQY4nAXGc=
github.com/rogpeppe/gohack v1.0.2/go.mod h1:DE8wqaJRPvHU0fden5cSYy7ar2dTbbccPT/eeOYcbcE=
github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@ -434,6 +439,7 @@ gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2/go.mo
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=