Compare commits

...

3 Commits

Author SHA1 Message Date
vilmibm cf99807126 stuff n things 2023-02-03 03:48:40 +00:00
vilmibm 65228979e9 add soem more cmd categories 2022-11-16 20:27:23 +00:00
vilmibm 75d64bf7b6 simplify this and make it parse new format 2022-10-30 23:09:06 +00:00
7 changed files with 511 additions and 69 deletions

View File

@ -111,6 +111,8 @@ func submit(opts *contribOpts) error {
"social",
"game",
"utility",
"programming",
"community",
"misc",
}
var choice int
@ -159,7 +161,7 @@ func submit(opts *contribOpts) error {
return fmt.Errorf("failed to serialize contrib: %w", err)
}
fname := fmt.Sprintf("%d", rand.Intn(10000))
fname := fmt.Sprintf("%d.yml", time.Now().Unix())
f, err := os.Create(path.Join(contribRequestPath, fname))
if err != nil {
return fmt.Errorf("failed to open contrib file for writing: %w", err)
@ -244,7 +246,8 @@ func rootCmd() *cobra.Command {
rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update")
rc.Flags().StringVarP(&delName, "delete", "d", "", "Name of command to delete")
rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command")
// TODO hiding this until i decide i want it or not
//rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command")
return rc
}

View File

@ -5,11 +5,9 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
townuser "git.tilde.town/tildetown/town/user"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@ -48,19 +46,7 @@ const binroot = "/town/commands"
var rootCmd = &cobra.Command{
Use: "town",
Short: "Run commands unique to tilde.town",
}
var adminCmd = &cobra.Command{
Use: "admin",
Short: "Run administrative commands",
}
func isAdmin() (bool, error) {
u, err := user.Current()
if err != nil {
return false, fmt.Errorf("failed to get information about current user: %w", err)
}
return townuser.IsAdmin(u)
// TODO Long example showing how to contribute a command
}
func parseCommands(targetCmd *cobra.Command, path string) error {
@ -79,61 +65,43 @@ func parseCommands(targetCmd *cobra.Command, path string) error {
return nil
}
type commandDoc struct {
ShortDesc string `yaml:"shortDesc"`
LongDesc string `yaml:"longDesc"`
Examples string
type cmdDoc struct {
CmdName string
ExecPath string
Maintainer string
Category string
ShortDesc string
LongDesc string
}
func parseCommand(targetCmd *cobra.Command, yamlPath string) {
executablePath := strings.TrimSuffix(yamlPath, ".yml")
// TODO handle when files lack executable bit
_, err := os.Stat(executablePath)
if err != nil {
fmt.Fprintf(os.Stderr, "could not find matching executable for %s; skipping...\n", yamlPath)
return
}
yamlBytes, err := ioutil.ReadFile(yamlPath)
if err != nil {
fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath)
return
}
doc := commandDoc{}
var doc cmdDoc
err = yaml.Unmarshal(yamlBytes, &doc)
if err != nil {
fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath)
return
}
if doc.Maintainer == "" {
fmt.Fprintf(os.Stderr, "%s is missing maintainer field; skipping...\n", yamlPath)
return
}
parsedCmd := &cobra.Command{
Use: filepath.Base(executablePath),
RunE: execWrapper(executablePath),
Use: doc.CmdName,
RunE: execWrapper(doc.ExecPath),
DisableFlagParsing: true,
}
if doc.ShortDesc != "" {
parsedCmd.Short = doc.ShortDesc
}
if doc.LongDesc != "" {
parsedCmd.Long = doc.LongDesc
}
parsedCmd.Long += fmt.Sprintf("\nMaintained by %s; reach out to them via mail or chat with questions", doc.Maintainer)
if doc.Examples != "" {
parsedCmd.Example = doc.Examples
}
targetCmd.AddCommand(parsedCmd)
}
@ -148,30 +116,14 @@ func execWrapper(executablePath string) func(*cobra.Command, []string) error {
}
func cli() int {
err := parseCommands(rootCmd, "core")
files, err := ioutil.ReadDir(binroot)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse core commands: %s", err)
return 1
fmt.Fprintf(os.Stderr, "failed to list directory %s: %s", binroot, err)
}
err = parseCommands(rootCmd, "contrib")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse contrib commands: %s", err)
return 1
}
admin, err := isAdmin()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to check admin status: %s", err)
return 2
}
if admin {
rootCmd.AddCommand(adminCmd)
err = parseCommands(adminCmd, "admin")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to parse admin commands: %s", err)
return 1
for _, file := range files {
if strings.HasSuffix(file.Name(), "yml") {
parseCommand(rootCmd, filepath.Join(binroot, file.Name()))
}
}

View File

@ -0,0 +1,61 @@
The point of this project is to enable signing up for tilde.town via an ssh connection.
It ought to work something like this:
```bash
ssh signup@tilde.town
<ascii art>
tilde.town
a creature stands before you. what does it look like?
> a floating gray cube
a calico cat with softly glowing eyes
a squid
something else
cube: you'd like to sign up for tilde.town, yes?
> yeah
nah
why should I?
you say, "yeah"
cube: by what name should i call you?
~cowcow________
cube: unfortunately, i already know someone by that name. what else can i call you?
~shelf_________
cube: excellent, hello ~shelf. have you been invited here by anyone i know?
> yes, i have an invite code
no
you say "yes, i have an invite code"
cube: great. please paste it and press enter:
_____________
```
...and so on.
TODO
- [ ] make signup user
- [ ] get dummy program to run as ssh handler
- [ ] tcell bootstrapping some kind of interactivity
- [ ] existing username check
- [ ] email validation
- [ ] collect responses
- [ ] create the signup request (on disk? db?)
- [ ] invite system support

366
cmd/signup/main.go 100644
View File

@ -0,0 +1,366 @@
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
"unicode/utf8"
"github.com/rivo/tview"
)
/*
Assumes:
Match User join
ForceCommand /town/src/town/cmd/signup/signup
PubkeyAuthentication no
KbdInteractiveAuthentication no
PasswordAuthentication yes
PermitEmptyPasswords yes
DisableForwarding yes
in sshd_config, and:
auth [success=done default=ignore] pam_succeed_if.so user ingroup join
in /etc/pam.d/sshd
*/
/*
is there a way :qa
*/
type streams struct {
In io.Reader
Out io.Writer
Err io.Writer
}
type Prompter struct {
in io.Reader
out io.Writer
width int
}
func (p *Prompter) Say(m string) {
for _, line := range wrap(m, p.width) {
fmt.Fprintln(p.out, line)
}
}
func (p *Prompter) CharSay(c, m string) {
sayPrefix := fmt.Sprintf("%s says: ", c)
prefixWidth := utf8.RuneCountInString(sayPrefix)
width := p.width - prefixWidth
indent := ""
for x := 0; x < utf8.RuneCountInString(sayPrefix); x++ {
indent += " "
}
for i, line := range wrap(m, width) {
if i == 0 {
fmt.Fprintln(p.out, sayPrefix+line)
} else {
fmt.Fprintln(p.out, indent+line)
}
}
}
type InputAnswer struct {
Value string
}
func runeLen(s string) int {
return utf8.RuneCountInString(s)
}
func wrap(s string, width int) []string {
fielded := strings.Fields(s)
out := []string{}
line := ""
for i, field := range fielded {
if runeLen(field)+runeLen(line)+1 < width {
line += field + " "
} else {
out = append(out, line)
line = field + " "
continue
}
if i == len(fielded)-1 {
out = append(out, line)
}
}
return out
}
func (p *Prompter) Confirm(m string) (bool, error) {
result := InputAnswer{}
for {
err := p.Ask(m, &result)
if err != nil {
return false, err
}
val := strings.TrimSpace(result.Value)
switch val {
case "yes", "y":
return true, nil
case "no", "n":
return false, nil
default:
p.Say("sorry, please say y or n")
p.Pause()
}
}
}
func (p *Prompter) Ask(m string, result *InputAnswer) error {
fmt.Fprintf(p.out, "%s ", m)
var val string
var err error
reader := bufio.NewReader(p.in)
for val == "" {
val, err = reader.ReadString('\n')
if err != nil {
return err
}
if val == "" {
p.Say("hmm, what was that?")
p.Pause()
}
}
result.Value = val
return nil
}
func (p *Prompter) AskLong(m string, result *InputAnswer) error {
fmt.Fprintf(p.out, "%s\n", m)
var val []byte
var err error
reader := bufio.NewReader(p.in)
for len(val) == 0 {
val, err = ioutil.ReadAll(reader)
if err != nil {
return err
}
if len(val) == 0 {
fmt.Fprintf(p.out, "%s\n", m)
}
}
result.Value = string(val)
return nil
}
func (p *Prompter) Pause() {
fmt.Fprintln(p.out)
time.Sleep(1 * time.Second)
}
func NewPrompter(width int, s *streams) Prompter {
return Prompter{
in: s.In,
out: s.Out,
width: width,
}
}
type answers struct {
username string
email string
applied time.Time
howDay string
howHeard string
reasons string
plans string
socials string
sshKey string
}
func _main(args []string, s *streams) error {
a := answers{}
p := NewPrompter(80, s)
// disable input buffering
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
// LOL i don't think this will work
reader := bufio.NewReader(p.in)
s, _ := reader.ReadString(4)
fmt.Printf("DBG %#v\n", s)
//var b []byte = make([]byte, 100)
//for {
// os.Stdin.Read(b)
// fmt.Println("I got the byte", b, "("+string(b)+")")
//}
var ia InputAnswer
if err := p.AskLong("lol give me stuff hit ctrl+d", &ia); err != nil {
return err
}
fmt.Printf("DBG %#v\n", ia.Value)
p.Say("you are standing in a field.")
p.Pause()
p.Say("there are flowers around you. you are standing in a slight depression and before you is grass touching a purple sky.")
p.Pause()
p.Say("you are not sure how long it's been when a cube about a meter high appears at the top of the hill before you.")
p.Say("the cube's surface is murky but iridescently reflective like an oil slick.")
p.Pause()
p.CharSay("cube", "hello. how is your day going?")
p.Say("(you can type a response and hit enter to talk to the cube)")
p.Pause()
howDay := InputAnswer{}
err := p.Ask("you say:", &howDay)
if err != nil {
return err
}
a.howDay = howDay.Value
p.Say("the cube inclines towards you gently as if nodding.")
p.CharSay("cube", "i see.")
p.Pause()
p.CharSay("cube", "i am guessing that if you are here, you want to be there.")
p.Pause()
p.Say("you blink and are somewhere else.")
p.Pause()
p.Say("the field of flowers is behind you and now you are up on the hill. the cube is next to you.")
p.Pause()
p.Say("you can see clear across a wide open plain. structures large and small dot the landscape. you catch a whiff of a savory smell and can just barely hear voices on the wind.")
p.Pause()
p.CharSay("cube", "the tilde town lies before us. if you want to continue, i'll ask you some questions about acquiring a home down in the town. you'll be free to edit your responses before i carry them off.")
p.Pause()
p.CharSay("cube", "first, i'm curious how you found out about the town?")
p.Pause()
answer := InputAnswer{}
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.howHeard = answer.Value
p.CharSay("cube", "interesting, thanks.")
p.CharSay("cube", "what sort of stuff do you want to get up to on the town?")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.plans = answer.Value
p.CharSay("cube", "thanks.")
p.CharSay("cube", "what do you like about the town?")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.reasons = answer.Value
p.CharSay("cube", "i appreciate it.")
p.CharSay("cube", "can you paste some links to other places you are active online? maybe a homepage or social media profile? if you aren't comfortable sharing or there are none, just say so.")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.socials = answer.Value
p.CharSay("cube", "ok, last thing. in order to break ground in the town, you'll need an SSH key. If you don't know what that is, you can check out this link: https://tilde.town/ssh.html .")
p.Pause()
if err := p.AskLong("paste SSH public key; press control+d to submit:", &answer); err != nil {
return err
}
a.sshKey = answer.Value
p.CharSay("cube", "i know that was a lot so i appreciate it. i've got everything written down here. before i carry it off, do you want to review and edit what you wrote?")
confirmed, err := p.Confirm("type y or n: ")
if err != nil {
return err
}
if confirmed {
err := edit(s, &a)
if err != nil {
return err
}
}
fmt.Printf("DBG %#v\n", confirmed)
fmt.Printf("DBG %#v\n", a)
// TODO allow for editing
// TODO write answers to disk
// TODO add a log
// TODO pretty colors
// TODO ascii art
// TODO IP throttling?
return nil
}
func edit(s *streams, a *answers) error {
// TODO make more real
/*
username string
email string
applied time.Time
howDay string
howHeard string
reasons string
plans string
socials string
sshKey string
*/
// TODO add note about tabbing around
app := tview.NewApplication()
form := tview.NewForm().
AddInputField("how did you hear about the town?", a.howHeard, 0, nil, nil).
AddButton("cool i'm good", nil).
AddButton("cancel and discard all this please", func() { app.Stop() })
form.SetBorder(true).SetTitle("edit your stuff").SetTitleAlign(tview.AlignCenter)
return app.SetRoot(form, true).EnableMouse(true).Run()
}
func main() {
retcode := 0
s := &streams{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
}
err := _main(os.Args, s)
if err != nil {
retcode = 1
fmt.Fprintln(os.Stderr, err)
}
os.Exit(retcode)
}

View File

@ -0,0 +1,46 @@
# New commands for signups
## town-signup
this binary is run by OpenSSH when a user anywhere in the world runs `ssh join@tilde.town`. It should:
- collect information about an application from a user
- allow them to edit their responses before submitting
- write a yaml file of their responses to disk
## review-signups
this binary is run by town admins in order to review, approve, notate, and reject applications. It should:
- iterate over signups
- allow fuzzy finding a particular signup
- allow approval using create-user
- allow rejection
- just move signup to archived rejected signup directory
- allow notating an application, ie:
- lock the yaml file for writing
- add notes to the yaml file that can be seen by other admins
- print info about historical signups
## create-user
this binary is called by `review-signups` to take a yaml application and create a user on disk. It should:
- create the user idempotently
- `adduser`
- `usermod` to set group
- calling `add-key` for user
- move the yaml file to an archive directory of approved signups
## user-key
this binary helps manage keys for users; basically automating the listing, adding, and removing of public keys for a user.
- `user-key add <username> <keyfile`
- `user-key list <username>`
- `user-key remove <username>`

7
go.mod
View File

@ -5,6 +5,7 @@ go 1.18
require (
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/charmbracelet/glamour v0.5.0
github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
)
@ -13,6 +14,8 @@ require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.5.3 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@ -25,12 +28,12 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.9.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/yuin/goldmark v1.4.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/text v0.3.7 // indirect
)

15
go.sum
View File

@ -16,6 +16,10 @@ 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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
@ -47,9 +51,12 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c h1:zIYU4PjQJ4BnYryMmpyizt1Un13V0ToCMXvC05DK8xc=
github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
@ -70,14 +77,18 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=