From ded34480e1e74b77a5d5a6e9550c6a5d94219346 Mon Sep 17 00:00:00 2001 From: mashiike Date: Thu, 27 Jan 2022 15:19:33 +0900 Subject: [PATCH] initial app --- .github/dependabot.yaml | 8 ++ .github/workflows/release.yaml | 28 +++++++ .github/workflows/test.yaml | 23 ++++++ .goreleaser.yml | 33 +++++++++ README.md | 79 ++++++++++++++++++++ cmd/flexentry/main.go | 46 ++++++++++++ executer.go | 132 +++++++++++++++++++++++++++++++++ executer_test.go | 72 ++++++++++++++++++ flexentry.go | 128 ++++++++++++++++++++++++++++++++ flexentry_test.go | 90 ++++++++++++++++++++++ go.mod | 24 ++++++ go.sum | 58 +++++++++++++++ 12 files changed, 721 insertions(+) create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .goreleaser.yml create mode 100644 cmd/flexentry/main.go create mode 100644 executer.go create mode 100644 executer_test.go create mode 100644 flexentry.go create mode 100644 flexentry_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..bb38f18 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + time: "20:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..c55696c --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +name: Release +on: + push: + branches: + - "!**/*" + tags: + - "v*.*.*" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: 1.17 + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v1 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..1bc6776 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,23 @@ +name: Test +on: [push] +jobs: + test: + strategy: + matrix: + go: + - 1.17 + name: Build + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Build & Test + run: | + go test -race ./... diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..dc2d835 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,33 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + - go mod download +builds: + - env: + - CGO_ENABLED=0 + main: ./cmd/flexentry + binary: flexentry + ldflags: + - -s -w + - -X main.Version=v{{.Version}} + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 +release: + prerelease: true +archives: +checksum: + name_template: "checksums.txt" +snapshot: + name_template: "{{ .Env.NIGHTLY_VERSION }}" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/README.md b/README.md index d22558c..45dd0f6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ # flexentry +![Latest GitHub release](https://img.shields.io/github/release/mashiike/flexentry.svg) +![Github Actions test](https://github.com/mashiike/flexentry/workflows/Test/badge.svg?branch=main) +[![Go Report Card](https://goreportcard.com/badge/mashiike/flexentry)](https://goreportcard.com/report/mashiike/flexentry) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mashiike/flexentry/blob/master/LICENSE) + Flexible entry point for Amazon ECS Task and Amazon Lambda container images + +## Usage + +```Dockerfile +FROM golang:1.17-buster + +RUN apt-get update && \ + apt-get install -y unzip && \ + apt-get clean + +RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ + unzip awscliv2.zip && \ + ./aws/install && \ + rm -R aws && \ + rm awscliv2.zip + +ARG FLEXENTRY_VERSION=0.0.0 +RUN curl -L https://github.com/mashiike/flexentry/releases/download/v${FLEXENTRY_VERSION}/flexentry_v${FLEXENTRY_VERSION}_linux_amd64.tar.gz | tar zxvf - && \ + install flexentry_v${FLEXENTRY_VERSION}_linux_amd64/flexentry /usr/local/bin/ + +ENTRYPOINT ["flexentry"] +``` + +Basically, all you have to do is specify the entry point of the container image. + +### Run on ECS Task + +```json +{ + "containerDefinitions": [ + { + "command": [ + "aws --version" + ], + "essential": true, + "image": "", + "name": "sample-app" + } + ], + "cpu": "256", + "executionRoleArn": "arn:aws:iam::012345678910:role/ecsTaskExecutionRole", + "family": "sample-task-definition", + "memory": "512", + "networkMode": "awsvpc", + "requiresCompatibilities": [ + "FARGATE" + ] +} +``` + +Decide what to execute with `command`, as in the task definition above. + +### Run on Lambda with container image + +If the environment variable `FLEXENTRY_COMMAND` is specified, the command will be executed. +Otherwise, the command to be executed will be determined according to the payload of the event. + +```json +{ + "cmd": "aws --version", + "description": "this is sample" +} +``` + +If the event payload is a string, it will be interpreted as a command. +Otherwise, by default, it looks at the `cmd` key to decide which command to execute. +To change the key, specify a jq expression for reference with `FLEXENTRY_COMMAND_JQ_EXPR`. +For example `export FLEXENTRY_COMMAND_JQ_EXPR=".command"` + +When executed by Amazon Lambda, the event payload is passed directly to the standard input as JSON data. + +## LICENSE + +MIT + diff --git a/cmd/flexentry/main.go b/cmd/flexentry/main.go new file mode 100644 index 0000000..838397d --- /dev/null +++ b/cmd/flexentry/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "log" + "os" + "strings" + "time" + + "github.com/fatih/color" + "github.com/fujiwara/logutils" + "github.com/mashiike/flexentry" +) + +var ( + Version string = "current" +) + +func main() { + logLevel := "info" + if l := os.Getenv("FLEXENTRY_LOG_LEVEL"); l != "" { + logLevel = l + } + filter := &logutils.LevelFilter{ + Levels: []logutils.LogLevel{"debug", "info", "warn", "error"}, + ModifierFuncs: []logutils.ModifierFunc{ + logutils.Color(color.FgHiBlack), + nil, + logutils.Color(color.FgYellow), + logutils.Color(color.FgRed, color.BgBlack), + }, + MinLevel: logutils.LogLevel(strings.ToLower(logLevel)), + Writer: os.Stderr, + } + log.SetOutput(filter) + log.Println("[debug] flexentry version:", Version) + entrypoint := flexentry.Entrypoint{ + Executer: flexentry.NewSSMWrapExecuter( + flexentry.NewShellExecuter(), + time.Minute, + ), + } + if err := entrypoint.Run(context.Background()); err != nil { + log.Fatalln("[error] ", err) + } +} diff --git a/executer.go b/executer.go new file mode 100644 index 0000000..fbc28fa --- /dev/null +++ b/executer.go @@ -0,0 +1,132 @@ +package flexentry + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "sync" + "time" + + "github.com/handlename/ssmwrap" +) + +type Executer interface { + Execute(ctx context.Context, stdin io.Reader, commands ...string) error +} + +type ShellExecuter struct { + shell string + shellArgs []string + + stdout io.Writer + stderr io.Writer +} + +func NewShellExecuter() *ShellExecuter { + return &ShellExecuter{ + shell: "sh", + shellArgs: []string{"-c"}, + stdout: os.Stdout, + stderr: os.Stderr, + } +} + +func (e *ShellExecuter) Execute(ctx context.Context, stdin io.Reader, commands ...string) error { + args := make([]string, 0, len(e.shellArgs)+len(commands)) + args = append(args, e.shellArgs...) + args = append(args, strings.Join(commands, " ")) + + log.Printf("[debug] $%s %s", e.shell, strings.Join(args, " ")) + cmd := exec.CommandContext(ctx, e.shell, args...) + cmd.Env = os.Environ() + p, _ := cmd.StdinPipe() + go func() { + defer p.Close() + if stdin == nil { + return + } + if _, err := io.Copy(p, stdin); err != nil { + log.Println("[warn] failed to write stdinPipe:", err) + } + }() + cmd.Stderr = e.stderr + cmd.Stdout = e.stdout + if err := cmd.Run(); err != nil { + return err + } + return nil +} + +func (e *ShellExecuter) Clone() *ShellExecuter { + cloned := *e + return &cloned +} + +func (e *ShellExecuter) SetShell(shell string, shellArgs []string) *ShellExecuter { + cloned := e.Clone() + cloned.shell = shell + cloned.shellArgs = make([]string, len(shellArgs)) + copy(cloned.shellArgs, shellArgs) + return cloned +} + +func (e *ShellExecuter) SetOutput(stdout, stderr io.Writer) *ShellExecuter { + cloned := e.Clone() + cloned.stdout = stdout + cloned.stderr = stderr + return cloned +} + +type SSMWrapExecuter struct { + Executer + + mu sync.Mutex + lastExported time.Time + ssmCacheExpires time.Duration +} + +func NewSSMWrapExecuter(executer Executer, cacheExpires time.Duration) *SSMWrapExecuter { + return &SSMWrapExecuter{ + Executer: executer, + ssmCacheExpires: cacheExpires, + } +} + +func (e *SSMWrapExecuter) exportEnvWithCache() error { + e.mu.Lock() + defer e.mu.Unlock() + if e.lastExported.IsZero() || e.lastExported.Before(time.Now().Add(-1*e.ssmCacheExpires)) { + defer func() { + e.lastExported = time.Now() + }() + return e.exportEnv() + } + log.Printf("[debug] exportEnv skipped. last exported at %s", e.lastExported.Format(time.RFC3339)) + return nil +} + +func (e *SSMWrapExecuter) exportEnv() error { + if paths := os.Getenv("SSMWRAP_PATHS"); paths == "" { + return nil + } else { + if err := ssmwrap.Export(ssmwrap.ExportOptions{ + Paths: strings.Split(paths, ","), + Retries: 3, + }); err != nil { + return fmt.Errorf("failed to fetch values from SSM paths %s: %w", paths, err) + } + log.Printf("[debug] exportEnv from SSMWRAP_PATHS=%s", paths) + } + return nil +} + +func (e *SSMWrapExecuter) Execute(ctx context.Context, stdin io.Reader, commands ...string) error { + if err := e.exportEnvWithCache(); err != nil { + return err + } + return e.Executer.Execute(ctx, stdin, commands...) +} diff --git a/executer_test.go b/executer_test.go new file mode 100644 index 0000000..e944b18 --- /dev/null +++ b/executer_test.go @@ -0,0 +1,72 @@ +package flexentry_test + +import ( + "bytes" + "context" + "os" + "strings" + "testing" + "time" + + "github.com/mashiike/flexentry" + "github.com/stretchr/testify/require" +) + +func TestExecuter(t *testing.T) { + testShell := "sh" + if s := os.Getenv("FLEXENTRY_TEST_SHELL"); s != "" { + testShell = s + } + testShellArgs := "-c" + if args := os.Getenv("FLEXENTRY_TEST_SHELL_ARGS"); args != "" { + testShellArgs = args + } + testShellExecuter := flexentry.NewShellExecuter(). + SetShell(testShell, strings.Split(testShellArgs, " ")) + + cases := []struct { + commands []string + stdin []byte + timeout time.Duration + exceptedErr string + exceptedStderr string + exceptedStdout string + }{ + { + commands: []string{"echo hoge"}, + exceptedStdout: "hoge\n", + }, + { + commands: []string{"hoge"}, + exceptedErr: "exit status 127", + exceptedStderr: "sh: hoge: command not found\n", + }, + { + commands: []string{"sleep 30"}, + timeout: 50 * time.Millisecond, + exceptedErr: "signal: killed", + }, + } + for _, c := range cases { + t.Run(strings.Join(c.commands, "_"), func(t *testing.T) { + stdin := bytes.NewReader(c.stdin) + var stdout, stderr bytes.Buffer + executer := testShellExecuter.SetOutput(&stdout, &stderr) + timeout := 1 * time.Minute + if c.timeout != 0 { + timeout = c.timeout + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + err := executer.Execute(ctx, stdin, c.commands...) + if c.exceptedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, c.exceptedErr) + } + require.EqualValues(t, c.exceptedStdout, stdout.String(), "stdout") + require.EqualValues(t, c.exceptedStderr, stderr.String(), "stderr") + + }) + } +} diff --git a/flexentry.go b/flexentry.go new file mode 100644 index 0000000..4c136f7 --- /dev/null +++ b/flexentry.go @@ -0,0 +1,128 @@ +package flexentry + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + "sync" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/itchyny/gojq" +) + +type Entrypoint struct { + Executer + + mu sync.Mutex + query *gojq.Query +} + +func (e *Entrypoint) Run(ctx context.Context) error { + if e.isLambda() { + log.Println("[debug] start lambda handler") + lambda.Start(e.handleRequest) + return nil + } + return e.Execute(ctx, os.Stdin, os.Args[1:]...) +} + +func (e *Entrypoint) isLambda() bool { + return strings.HasPrefix(os.Getenv("AWS_EXECUTION_ENV"), "AWS_Lambda") || + os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" +} + +type Event interface{} + +func (e *Entrypoint) Execute(ctx context.Context, stdin io.Reader, commands ...string) error { + if e.Executer == nil { + return nil + } + return e.Executer.Execute(ctx, stdin, commands...) +} + +func (e *Entrypoint) handleRequest(ctx context.Context, event Event) error { + commands, err := e.DetectCommand(ctx, event) + if err != nil { + log.Println("[error] ", err) + return err + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(event); err != nil { + log.Println("[error] failed event encode", err) + return err + } + err = e.Execute(ctx, &buf, commands...) + if err != nil { + log.Println("[error] ", err) + return err + } + return err +} + +func (e *Entrypoint) DetectCommand(ctx context.Context, event Event) ([]string, error) { + if command := os.Getenv("FLEXENTRY_COMMAND"); command != "" { + return []string{command}, nil + } + if command, ok := event.(string); ok { + return []string{command}, nil + } + if commands, ok := event.([]string); ok { + return commands, nil + } + if data, ok := event.(map[string]interface{}); ok { + query, err := e.getQuery() + if err != nil { + return nil, err + } + commands := make([]string, 0, 1) + iter := query.RunWithContext(ctx, data) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, ok := v.(error); ok { + return nil, fmt.Errorf("command parse failed: %w", err) + } + if command, ok := v.(string); ok { + commands = append(commands, command) + continue + } + if cs, ok := v.([]string); ok { + commands = append(commands, cs...) + continue + } + if num, ok := v.(int); ok { + commands = append(commands, strconv.Itoa(num)) + } + } + return commands, nil + } + + return nil, errors.New("FLEXENTRY_COMMAND is required") +} + +func (e *Entrypoint) getQuery() (*gojq.Query, error) { + e.mu.Lock() + defer e.mu.Unlock() + if e.query != nil { + return e.query, nil + } + jqExpr := ".cmd" + if j := os.Getenv("FLEXENTRY_COMMAND_JQ_EXPR"); j != "" { + jqExpr = j + } + var err error + e.query, err = gojq.Parse(jqExpr) + if err != nil { + return nil, err + } + return e.query, nil +} diff --git a/flexentry_test.go b/flexentry_test.go new file mode 100644 index 0000000..b8a4442 --- /dev/null +++ b/flexentry_test.go @@ -0,0 +1,90 @@ +package flexentry_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/mashiike/flexentry" + "github.com/stretchr/testify/require" +) + +func TestEntrypointDetectCommand(t *testing.T) { + cases := []struct { + preAction func() + postAction func() + event flexentry.Event + expected []string + }{ + { + event: "echo hoge", + expected: []string{"echo hoge"}, + }, + { + preAction: func() { + os.Setenv("FLEXENTRY_COMMAND", "echo hoge") + }, + postAction: func() { + os.Unsetenv("FLEXENTRY_COMMAND") + }, + expected: []string{"echo hoge"}, + }, + { + event: map[string]interface{}{ + "cmd": "echo hoge", + }, + expected: []string{"echo hoge"}, + }, + { + preAction: func() { + os.Setenv("FLEXENTRY_COMMAND_JQ_EXPR", ".cmd2") + }, + postAction: func() { + os.Unsetenv("FLEXENTRY_COMMAND_JQ_EXPR") + }, + event: map[string]interface{}{ + "cmd": "echo hoge", + "cmd2": "echo fuga", + }, + expected: []string{"echo fuga"}, + }, + { + event: []string{"echo", "hoge"}, + expected: []string{"echo", "hoge"}, + }, + { + preAction: func() { + os.Setenv("FLEXENTRY_COMMAND_JQ_EXPR", ".cmd | ..") + }, + postAction: func() { + os.Unsetenv("FLEXENTRY_COMMAND_JQ_EXPR") + }, + event: map[string]interface{}{ + "cmd": []interface{}{"echo", 1}, + }, + expected: []string{"echo", "1"}, + }, + { + event: map[string]interface{}{ + "cmd": []string{"echo", "fuga"}, + }, + expected: []string{"echo", "fuga"}, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case.%d", i), func(t *testing.T) { + if c.preAction != nil { + c.preAction() + } + if c.postAction != nil { + defer c.postAction() + } + e := &flexentry.Entrypoint{} + actual, err := e.DetectCommand(context.Background(), c.event) + require.NoError(t, err) + require.EqualValues(t, c.expected, actual) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..65550cb --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/mashiike/flexentry + +go 1.17 + +require ( + github.com/aws/aws-lambda-go v1.28.0 + github.com/fatih/color v1.13.0 + github.com/fujiwara/logutils v1.1.0 + github.com/handlename/ssmwrap v1.1.1 + github.com/itchyny/gojq v0.12.6 + github.com/stretchr/testify v1.7.0 +) + +require ( + github.com/aws/aws-sdk-go v1.30.19 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/itchyny/timefmt-go v0.1.3 // indirect + github.com/jmespath/go-jmespath v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..45fc1aa --- /dev/null +++ b/go.sum @@ -0,0 +1,58 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-lambda-go v1.28.0 h1:fZiik1PZqW2IyAN4rj+Y0UBaO1IDFlsNo9Zz/XnArK4= +github.com/aws/aws-lambda-go v1.28.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/aws/aws-sdk-go v1.30.19 h1:vRwsYgbUvC25Cb3oKXTyTYk3R5n1LRVk8zbvL4inWsc= +github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fujiwara/logutils v1.1.0 h1:JAYmqW40d/ZjzouB01sfZiaTxwNe4hwmB6lLajZqm1s= +github.com/fujiwara/logutils v1.1.0/go.mod h1:pdb/Uk70rjQWEmFm/OvYH7OG8meZt1fEIqC0qZbvro4= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/handlename/ssmwrap v1.1.1 h1:mLv6b7Sq/PhA2cjFH/sbSu4g/VoTWNgdsFCqJ3fHzxE= +github.com/handlename/ssmwrap v1.1.1/go.mod h1:vF1fjedJ5a0CQ+JBmdGLHwznLHGLvI9sy5X6N1itFMU= +github.com/itchyny/gojq v0.12.6 h1:VjaFn59Em2wTxDNGcrRkDK9ZHMNa8IksOgL13sLL4d0= +github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItqsfP0A= +github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=