Skip to content

Commit

Permalink
Merge pull request #973 from ripienaar/cluster_balance
Browse files Browse the repository at this point in the history
Adds a cluster connection balancer
  • Loading branch information
ripienaar authored Jan 18, 2024
2 parents 1c031c1 + ae9a839 commit e8fede9
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 39 deletions.
8 changes: 4 additions & 4 deletions cli/auth_account_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ func configureAuthAccountCommand(auth commandHost) {
push.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
push.Flag("show", "Show the Account JWT before pushing").UnNegatableBoolVar(&c.showJWT)

pull := acct.Command("query", "Pull the account from the NATS Resolver and view it").Action(c.pullAction)
pull.Arg("name", "Account to act on").Required().StringVar(&c.accountName)
pull.Arg("output", "Saves the JWT to a file").StringVar(&c.output)
query := acct.Command("query", "Pull the account from the NATS Resolver and view it").Alias("pull").Action(c.queryAction)
query.Arg("name", "Account to act on").Required().StringVar(&c.accountName)
query.Arg("output", "Saves the JWT to a file").StringVar(&c.output)

sk := acct.Command("keys", "Manage Scoped Signing Keys").Alias("sk").Alias("s")

Expand Down Expand Up @@ -185,7 +185,7 @@ func (c *authAccountCommand) selectOperator(pick bool) (*ab.AuthImpl, ab.Operato
return auth, oper, err
}

func (c *authAccountCommand) pullAction(_ *fisk.ParseContext) error {
func (c *authAccountCommand) queryAction(_ *fisk.ParseContext) error {
nc, _, err := prepareHelper("", natsOpts()...)
if err != nil {
return err
Expand Down
87 changes: 76 additions & 11 deletions cli/server_cluster_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,96 @@ import (

"github.com/choria-io/fisk"
"github.com/nats-io/jsm.go/api"
"github.com/nats-io/jsm.go/connbalancer"
"github.com/nats-io/nats-server/v2/server"
)

type SrvClusterCmd struct {
json bool
force bool
peer string
placementCluster string
json bool
force bool
peer string
placementCluster string
balanceServerName string
balanceIdle time.Duration
balanceAccount string
balanceSubject string
balanceRunTime time.Duration
balanceKinds []string
}

func configureServerClusterCommand(srv *fisk.CmdClause) {
c := &SrvClusterCmd{}

raft := srv.Command("cluster", "Manage JetStream Clustering").Alias("r").Alias("raft")
raft.Flag("json", "Produce JSON output").Short('j').UnNegatableBoolVar(&c.json)
cluster := srv.Command("cluster", "Manage JetStream Clustering").Alias("r").Alias("raft")

sd := raft.Command("step-down", "Force a new leader election by standing down the current meta leader").Alias("stepdown").Alias("sd").Alias("elect").Alias("down").Alias("d").Action(c.metaLeaderStandDown)
balance := cluster.Command("balance", "Balance cluster connections").Action(c.balanceAction)
balance.Arg("duration", "Spread balance requests over a certain duration").Default("2m").DurationVar(&c.balanceRunTime)
balance.Flag("server-name", "Restrict balancing to a specific server").PlaceHolder("NAME").StringVar(&c.balanceServerName)
balance.Flag("idle", "Balance connections that has been idle for a period").PlaceHolder("DURATION").DurationVar(&c.balanceIdle)
balance.Flag("account", "Balance connections in a certain account only").StringVar(&c.balanceAccount)
balance.Flag("subject", "Balance connections interested in certain subjects").StringVar(&c.balanceSubject)
balance.Flag("kind", "Balance only certain kinds of connection (*Client, Leafnode)").Default("Client").EnumsVar(&c.balanceKinds, "Client", "Leafnode")
balance.Flag("force", "Force rebalance without prompting").Short('f').UnNegatableBoolVar(&c.force)

sd := cluster.Command("step-down", "Force a new leader election by standing down the current meta leader").Alias("stepdown").Alias("sd").Alias("elect").Alias("down").Alias("d").Action(c.metaLeaderStandDownAction)
sd.Flag("cluster", "Request placement of the leader in a specific cluster").StringVar(&c.placementCluster)
sd.Flag("json", "Produce JSON output").Short('j').UnNegatableBoolVar(&c.json)

rm := raft.Command("peer-remove", "Removes a server from a JetStream cluster").Alias("rm").Alias("pr").Action(c.metaPeerRemove)
rm := cluster.Command("peer-remove", "Removes a server from a JetStream cluster").Alias("rm").Alias("pr").Action(c.metaPeerRemoveAction)
rm.Arg("name", "The Server Name or ID to remove from the JetStream cluster").Required().StringVar(&c.peer)
rm.Flag("force", "Force removal without prompting").Short('f').UnNegatableBoolVar(&c.force)
rm.Flag("json", "Produce JSON output").Short('j').UnNegatableBoolVar(&c.json)
}

func (c *SrvClusterCmd) balanceAction(_ *fisk.ParseContext) error {
if !c.force {
fmt.Println("Re-balancing will disconnect clients without knowing their current state.")
fmt.Println()
fmt.Println("The clients will trigger normal reconnect behavior. This can interrupt in-flight work.")
fmt.Println()
ok, err := askConfirmation("Really re-balance connections", false)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("balance canceled")
}
}

nc, _, err := prepareHelper("", natsOpts()...)
if err != nil {
return err
}

level := connbalancer.InfoLevel
if opts.Trace {
level = connbalancer.TraceLevel
}

balancer, err := connbalancer.New(nc, c.balanceRunTime, connbalancer.NewDefaultLogger(level), connbalancer.ConnectionSelector{
ServerName: c.balanceServerName,
Idle: c.balanceIdle,
Account: c.balanceAccount,
SubjectInterest: c.balanceSubject,
Kind: c.balanceKinds,
})
if err != nil {
return err
}

balanced, err := balancer.Balance(ctx)
if err != nil {
return err
}

fmt.Println()

fmt.Printf("Balanced %s connections\n", f(balanced))

return nil
}

func (c *SrvClusterCmd) metaPeerRemove(_ *fisk.ParseContext) error {
func (c *SrvClusterCmd) metaPeerRemoveAction(_ *fisk.ParseContext) error {
nc, mgr, err := prepareHelper("", natsOpts()...)
if err != nil {
return err
Expand Down Expand Up @@ -92,7 +157,7 @@ func (c *SrvClusterCmd) metaPeerRemove(_ *fisk.ParseContext) error {
}

if !c.force {
fmt.Printf("Removing %s can not be reversed, data on this node will be inaccessible.\n\n", c.peer)
fmt.Printf("Removing %s can not be reversed, data on this node will be inaccessible and the server name can not be used again. You should only remove nodes that will not return in future.\n\n", c.peer)

var remove bool
if c.peer == foundName || strings.Contains(foundName, foundID) {
Expand All @@ -117,7 +182,7 @@ func (c *SrvClusterCmd) metaPeerRemove(_ *fisk.ParseContext) error {
return nil
}

func (c *SrvClusterCmd) metaLeaderStandDown(_ *fisk.ParseContext) error {
func (c *SrvClusterCmd) metaLeaderStandDownAction(_ *fisk.ParseContext) error {
nc, mgr, err := prepareHelper("", natsOpts()...)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cli/server_watch_acct_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The 'Servers' column will show how many servers sent statistics about an account
Only servers with active connections will send these updates.
`)
accounts.Flag("sort", fmt.Sprintf("Sorts by a specific property (%s)", strings.Join(sortKeys, ", "))).Default("conns").EnumVar(&c.sort, sortKeys...)
accounts.Flag("number", "Amount of Accounts to show by the selected dimension").Default("10").Short('n').IntVar(&c.topCount)
accounts.Flag("number", "Amount of Accounts to show by the selected dimension").Default("0").Short('n').IntVar(&c.topCount)
}

func (c *SrvWatchAccountCmd) accountsAction(_ *fisk.ParseContext) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/server_watch_js_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func configureServerWatchJSCommand(watch *fisk.CmdClause) {
Since the updates are sent on a 30 second interval this is not a point in time view.
`)
js.Flag("sort", fmt.Sprintf("Sorts by a specific property (%s)", strings.Join(sortKeys, ", "))).Default("assets").EnumVar(&c.sort, sortKeys...)
js.Flag("number", "Amount of Accounts to show by the selected dimension").Default("10").Short('n').IntVar(&c.topCount)
js.Flag("number", "Amount of Accounts to show by the selected dimension").Default("0").Short('n').IntVar(&c.topCount)
}

func (c *SrvWatchJSCmd) jetstreamAction(_ *fisk.ParseContext) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/server_watch_srv_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func configureServerWatchServerCommand(watch *fisk.CmdClause) {
Since the updates are sent on a 30 second interval this is not a point in time view.
`)
servers.Flag("sort", fmt.Sprintf("Sorts by a specific property (%s)", strings.Join(sortKeys, ", "))).Default("conns").EnumVar(&c.sort, sortKeys...)
servers.Flag("number", "Amount of Accounts to show by the selected dimension").Default("10").Short('n').IntVar(&c.topCount)
servers.Flag("number", "Amount of Accounts to show by the selected dimension").Default("0").Short('n').IntVar(&c.topCount)
}

func (c *SrvWatchServerCmd) serversAction(_ *fisk.ParseContext) error {
Expand Down
6 changes: 3 additions & 3 deletions cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,14 +871,14 @@ func doReqAsync(req any, subj string, waitFor int, nc *nats.Conn, cb func([]byte
}

var (
mu sync.Mutex
ctr = 0
mu sync.Mutex
ctr = 0
finisher *time.Timer
)

ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel()

var finisher *time.Timer
if waitFor == 0 {
finisher = time.NewTimer(opts.Timeout)
go func() {
Expand Down
11 changes: 5 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ require (
github.com/HdrHistogram/hdrhistogram-go v1.1.2
github.com/choria-io/fisk v0.6.2
github.com/dustin/go-humanize v1.0.1
github.com/emicklei/dot v1.6.0
github.com/emicklei/dot v1.6.1
github.com/expr-lang/expr v1.15.8
github.com/fatih/color v1.16.0
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gosuri/uiprogress v0.0.1
github.com/guptarohit/asciigraph v0.5.6
github.com/jedib0t/go-pretty/v6 v6.5.2
github.com/jedib0t/go-pretty/v6 v6.5.3
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.4
github.com/mattn/go-isatty v0.0.20
github.com/nats-io/jsm.go v0.1.1-0.20240111112330-8508bd502b64
github.com/nats-io/jsm.go v0.1.1-0.20240118115416-fcaa77de81f6
github.com/nats-io/jwt/v2 v2.5.3
github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20240111031832-67d41da49bb2
github.com/nats-io/nats.go v1.32.0
github.com/nats-io/nkeys v0.4.7
github.com/nats-io/nuid v1.0.1
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/common v0.45.0
github.com/prometheus/common v0.46.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/synadia-io/jwt-auth-builder.go v0.0.0-20240116162838-d0188a7c69ae
github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
golang.org/x/term v0.16.0
gopkg.in/gizak/termui.v1 v1.0.0-20151021151108-e62b5929642a
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -43,7 +43,6 @@ require (
github.com/gosuri/uilive v0.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
Expand Down
22 changes: 10 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/dot v1.6.0 h1:vUzuoVE8ipzS7QkES4UfxdpCwdU2U97m2Pb2tQCoYRY=
github.com/emicklei/dot v1.6.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/emicklei/dot v1.6.1 h1:ujpDlBkkwgWUY+qPId5IwapRW/xEoligRSYjioR6DFI=
github.com/emicklei/dot v1.6.1/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/expr-lang/expr v1.15.8 h1:FL8+d3rSSP4tmK9o+vKfSMqqpGL8n15pEPiHcnBpxoI=
github.com/expr-lang/expr v1.15.8/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
Expand All @@ -46,8 +46,8 @@ github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL
github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/jedib0t/go-pretty/v6 v6.5.2 h1:1zphkAo77tdoCkdqIYsMHoXmEGTnTy3GZ6Mn+NyIro0=
github.com/jedib0t/go-pretty/v6 v6.5.2/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
github.com/jedib0t/go-pretty/v6 v6.5.3 h1:GIXn6Er/anHTkVUoufs7ptEvxdD6KIhR7Axa2wYCPF0=
github.com/jedib0t/go-pretty/v6 v6.5.3/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
Expand All @@ -68,17 +68,15 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/nats-io/jsm.go v0.1.1-0.20240111112330-8508bd502b64 h1:I0hHfTI0Kei9Mx4fmME49QuHWqdw8LNh1qUUsqOh7tk=
github.com/nats-io/jsm.go v0.1.1-0.20240111112330-8508bd502b64/go.mod h1:D74tqFdGsmWJ9Eo77tLWEJ8FU4oGcXWegRV3+b0BQps=
github.com/nats-io/jsm.go v0.1.1-0.20240118115416-fcaa77de81f6 h1:Ux6MzBV3LOvoGH6RRen5bXGmtlEHc+RAWKMk49zCWzo=
github.com/nats-io/jsm.go v0.1.1-0.20240118115416-fcaa77de81f6/go.mod h1:ZJ2U65E3DwQDlTd8CmqnnRnN/wfo51vO0LGRB5UQ2UM=
github.com/nats-io/jwt/v2 v2.5.3 h1:/9SWvzc6hTfamcgXJ3uYRpgj+QuY2aLNqRiqrKcrpEo=
github.com/nats-io/jwt/v2 v2.5.3/go.mod h1:iysuPemFcc7p4IoYots3IuELSI4EDe9Y0bQMe+I3Bf4=
github.com/nats-io/nats-server/v2 v2.11.0-dev.0.20240111031832-67d41da49bb2 h1:3Pb4Jp6AiJl9htNdLO4A92Gj6j88S87OWZModvkzDrE=
Expand All @@ -100,8 +98,8 @@ github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand Down Expand Up @@ -129,8 +127,8 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
Expand Down

0 comments on commit e8fede9

Please sign in to comment.