├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── dependabot-auto-merge.yml │ ├── gha-lint.yml │ └── image.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── entrypoint.sh ├── examples ├── go │ ├── Dockerfile │ └── main.go └── shell │ ├── Dockerfile │ └── test.sh ├── function.go ├── function_test.go ├── go.mod ├── go.sum ├── lapper.png ├── main.go └── slack.go /.dockerignore: -------------------------------------------------------------------------------- 1 | lapper 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | time: "12:00" 8 | timezone: "Asia/Tokyo" 9 | # To avoid lock file conflicts, group all updates together. 10 | groups: 11 | gomod-all: 12 | patterns: 13 | - "*" 14 | - package-ecosystem: "docker" 15 | directories: 16 | - "/" 17 | schedule: 18 | interval: "monthly" 19 | time: "12:00" 20 | timezone: "Asia/Tokyo" 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "monthly" 25 | time: "12:00" 26 | timezone: "Asia/Tokyo" 27 | groups: 28 | github-actions-all: 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened. 5 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 6 | # So we add default event types and ready_for_review type here. 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | - ready_for_review 13 | 14 | permissions: 15 | contents: read 16 | pull-requests: read 17 | 18 | jobs: 19 | test: 20 | timeout-minutes: 10 21 | if: github.event.pull_request.draft == false 22 | name: Run test 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 27 | with: 28 | go-version-file: go.mod 29 | - run: make test 30 | 31 | lint: 32 | timeout-minutes: 10 33 | if: github.event.pull_request.draft == false 34 | name: Run lint 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 40 | with: 41 | only-new-issues: true 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | # 3 | # Our release process is not automated, so auto-merge risk can be considered low. 4 | name: Dependabot auto-merge 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | dependabot: 13 | timeout-minutes: 5 14 | runs-on: ubuntu-latest 15 | if: github.actor == 'dependabot[bot]' 16 | steps: 17 | - name: Dependabot metadata 18 | id: metadata 19 | uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34 # v2.2.0 20 | with: 21 | github-token: "${{ secrets.GITHUB_TOKEN }}" 22 | - name: Enable auto-merge for Dependabot PRs 23 | # github-actions dependencies are used immediately, so avoid auto-merging them. 24 | if: | 25 | steps.metadata.outputs.package-ecosystem != 'github_actions' && 26 | steps.metadata.outputs.update-type != 'version-update:semver-major' 27 | run: gh pr merge --auto --squash "${PR_URL}" 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | -------------------------------------------------------------------------------- /.github/workflows/gha-lint.yml: -------------------------------------------------------------------------------- 1 | name: Call gha-lint 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - synchronize 7 | - reopened 8 | # Manually trigger this workflow to pass required status check 9 | - ready_for_review 10 | paths: 11 | - '.github/**' 12 | jobs: 13 | call-gha-lint: 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | uses: Finatext/workflows-public/.github/workflows/gha-lint.yml@main 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.github/workflows/image.yml: -------------------------------------------------------------------------------- 1 | name: lapper 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '0 0 1 * *' 10 | permissions: 11 | contents: read 12 | packages: write 13 | jobs: 14 | image: 15 | name: Image 16 | runs-on: ubuntu-22.04 17 | env: 18 | IMAGE_NAME: docker.pkg.github.com/finatext/lapper/lapper:beta 19 | IMAGE_NAME_LATEST: docker.pkg.github.com/finatext/lapper/lapper:latest 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 1 24 | - name: Test 25 | run: make test 26 | - name: Build 27 | run: | 28 | docker build -t $IMAGE_NAME . 29 | docker build -t $IMAGE_NAME_LATEST . 30 | - name: Docker Login 31 | run: | 32 | docker login docker.pkg.github.com -u owner -p ${{ secrets.GITHUB_TOKEN }} 33 | - name: Push 34 | if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule' 35 | run: | 36 | docker push $IMAGE_NAME 37 | docker push $IMAGE_NAME_LATEST 38 | timeout-minutes: 5 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lapper 2 | sample/sample 3 | .env 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | ENV APPDIR=$GOPATH/src/github.com/Finatext/lapper 3 | ENV GO111MODULE=on 4 | RUN apk update && apk add --no-cache git && mkdir -p $APPDIR 5 | ADD . $APPDIR/ 6 | WORKDIR $APPDIR 7 | RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o lapper *.go 8 | 9 | FROM public.ecr.aws/lambda/provided:al2 10 | COPY --from=builder /go/src/github.com/Finatext/lapper/lapper ${LAMBDA_RUNTIME_DIR}/bootstrap 11 | ADD entrypoint.sh ${LAMBDA_TASK_ROOT} 12 | ENTRYPOINT ["/lambda-entrypoint.sh"] 13 | CMD ["./entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Finatext Holdings Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: lapper/build examples/go/build examples/shell/build 2 | 3 | 4 | test: 5 | go test 6 | 7 | lapper/build: 8 | docker build -t lapper:latest . 9 | 10 | examples/%/build: 11 | docker build -t lapper-example-$*:latest examples/$*/ 12 | 13 | examples/%/run: 14 | docker run --env-file .env --rm -p 9000:8080 lapper-example-$*:latest 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lapper (Lambda wrapper) 2 | ========= 3 | 4 | ![lapper](https://github.com/Finatext/lapper/workflows/lapper/badge.svg) 5 | 6 | **! Lapper is still in Beta version !** 7 | Lapper is a wrapper container for Lambda Functions. 8 | 9 | ![lapper.png](./lapper.png) 10 | 11 | ## Concept 12 | 13 | * Easy to add notification on your function. 14 | * Function Language agnostic. 15 | 16 | ## Usage 17 | 18 | ### 1. Build Container Image 19 | 20 | 21 | Build your container image from lapper container image. 22 | 23 | ``` 24 | FROM docker.pkg.github.com/finatext/lapper/lapper:beta 25 | WORKDIR ${LAMBDA_TASK_ROOT} 26 | COPY test.sh ${LAMBDA_TASK_ROOT} 27 | CMD ["./test.sh"] 28 | ``` 29 | 30 | ### 2. Set Environment Variables 31 | 32 | You can use these variables. 33 | 34 | * `LAPPER_SLACK_WEBHOOK_URL`(Optional) ... Slack Webhook URL for notification. 35 | * `LAPPER_NOTIFY_COND`(Optional, Default:stderr) ... Specify when to notify. 36 | * `all` ... Notify every time. 37 | * `stderr` ... Notify if stderr was output. 38 | * `exitcode` ... Notify if exit code wasn't 0. 39 | * `LAPPER_POST_STDOUT`(Optional, Default:`false`) ... Whether includes STDOUT on message. 40 | * `LAPPER_POST_STDERR`(Optional, Default:`true`) ... Whether includes STDERR on message. 41 | * `LAPPER_POST_ERROR`(Optional, Default:`true`) ... Whether includes error on message. 42 | 43 | ## Examples 44 | 45 | * [Go](./examples/go/) 46 | * [Shell Script](./examples/shell/) 47 | 48 | ## License 49 | 50 | [MIT](./LICENSE.md) 51 | 52 | ## Author 53 | 54 | [Satoshi Tajima](https://github.com/s-tajima) 55 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo hello lapper 4 | -------------------------------------------------------------------------------- /examples/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | ENV APPDIR=$GOPATH/src/github.com/Finatext/lapper/examples/go 3 | ENV GO111MODULE=on 4 | RUN apk update && rm -rf /var/cache/apk/* && mkdir -p $APPDIR 5 | ADD . $APPDIR/ 6 | WORKDIR $APPDIR 7 | RUN CGO_ENABLED=0 go build -mod=vendor -ldflags "-s -w" -o go main.go 8 | 9 | FROM lapper 10 | WORKDIR ${LAMBDA_TASK_ROOT} 11 | COPY --from=builder /go/src/github.com/Finatext/lapper/examples/go ${LAMBDA_TASK_ROOT} 12 | CMD ["./go"] 13 | -------------------------------------------------------------------------------- /examples/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | stdin, _ := ioutil.ReadAll(os.Stdin) 12 | 13 | fmt.Println("* This is STDIN: ", string(stdin)) 14 | fmt.Println("* This is STDOUT") 15 | 16 | time.Sleep(500 * time.Millisecond) 17 | 18 | fmt.Println("* This is STDOUT") 19 | 20 | time.Sleep(500 * time.Millisecond) 21 | 22 | fmt.Fprint(os.Stderr, "* This is STDERR\n") 23 | 24 | time.Sleep(500 * time.Millisecond) 25 | 26 | fmt.Println("* This is STDOUT") 27 | 28 | time.Sleep(500 * time.Millisecond) 29 | 30 | fmt.Fprint(os.Stderr, "* This is STDERR\n") 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /examples/shell/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lapper:latest 2 | WORKDIR ${LAMBDA_TASK_ROOT} 3 | COPY test.sh ${LAMBDA_TASK_ROOT} 4 | CMD ["./test.sh"] 5 | -------------------------------------------------------------------------------- /examples/shell/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "This is STDOUT" 4 | echo "This is STDERR" >&2 5 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "sync" 12 | ) 13 | 14 | const ( 15 | initialBufSize = 65536 16 | maxBufSize = 6553600 17 | ) 18 | 19 | type Function struct { 20 | Command string 21 | Args []string 22 | Payload []byte 23 | Stdout io.Writer 24 | Stderr io.Writer 25 | } 26 | 27 | func NewFunction(command string, args []string, payload []byte) *Function { 28 | f := &Function{ 29 | Command: command, 30 | Args: args, 31 | Payload: payload, 32 | Stdout: os.Stdout, 33 | Stderr: os.Stderr, 34 | } 35 | 36 | return f 37 | } 38 | 39 | func (fn *Function) SetStdout(w io.Writer) { 40 | fn.Stdout = w 41 | } 42 | 43 | func (fn *Function) SetStderr(w io.Writer) { 44 | fn.Stderr = w 45 | } 46 | 47 | func (fn *Function) Run() (string, string, error) { 48 | cmd := exec.Command(fn.Command, fn.Args...) 49 | 50 | stdout, err := cmd.StdoutPipe() 51 | if err != nil { 52 | return "", "", fmt.Errorf("failed to open stdout pipe: %w", err) 53 | } 54 | 55 | stderr, err := cmd.StderrPipe() 56 | if err != nil { 57 | return "", "", fmt.Errorf("failed to open stderr pipe: %w", err) 58 | } 59 | 60 | stdin, err := cmd.StdinPipe() 61 | if err != nil { 62 | return "", "", fmt.Errorf("failed to open stdin pipe: %w", err) 63 | } 64 | 65 | var stdout1, stderr1 bytes.Buffer 66 | stdout2 := io.TeeReader(stdout, &stdout1) 67 | stderr2 := io.TeeReader(stderr, &stderr1) 68 | 69 | err = cmd.Start() 70 | if err != nil { 71 | return "", "", fmt.Errorf("failed to start command: %w", err) 72 | } 73 | 74 | if _, err := stdin.Write(fn.Payload); err != nil { 75 | return "", "", fmt.Errorf("failed to write to stdin: %w", err) 76 | } 77 | stdin.Close() 78 | 79 | wg := &sync.WaitGroup{} 80 | 81 | wg.Add(1) 82 | go func() { 83 | scanner := bufio.NewScanner(stdout2) 84 | buf := make([]byte, initialBufSize) 85 | scanner.Buffer(buf, maxBufSize) 86 | 87 | for scanner.Scan() { 88 | fmt.Fprintln(fn.Stdout, scanner.Text()) 89 | } 90 | if err := scanner.Err(); err != nil { 91 | fmt.Fprintln(fn.Stderr, "scan error (stdout):", err) 92 | } 93 | wg.Done() 94 | }() 95 | 96 | wg.Add(1) 97 | go func() { 98 | scanner := bufio.NewScanner(stderr2) 99 | buf := make([]byte, initialBufSize) 100 | scanner.Buffer(buf, maxBufSize) 101 | 102 | for scanner.Scan() { 103 | fmt.Fprintln(fn.Stderr, scanner.Text()) 104 | } 105 | if err := scanner.Err(); err != nil { 106 | fmt.Fprintln(fn.Stderr, "scan error (stderr):", err) 107 | } 108 | wg.Done() 109 | }() 110 | 111 | wg.Wait() 112 | err = cmd.Wait() 113 | // Before checking command success, we need to collect all the output 114 | stdoutBytes := stdout1.String() 115 | stderrBytes := stderr1.String() 116 | 117 | if err != nil { 118 | var exitError *exec.ExitError 119 | if ok := errors.As(err, &exitError); ok { 120 | return stdoutBytes, stderrBytes, fmt.Errorf("command failed with exit code %d", exitError.ExitCode()) 121 | } else { 122 | // If this is not an ExitError, it's a unexpected situation 123 | return stderrBytes, stderrBytes, fmt.Errorf("failed to execute command: %w", err) 124 | } 125 | } 126 | 127 | return stdoutBytes, stderrBytes, nil 128 | } 129 | -------------------------------------------------------------------------------- /function_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | fuzz "github.com/google/gofuzz" 9 | ) 10 | 11 | func TestRunSimple(t *testing.T) { 12 | cases := []struct { 13 | command string 14 | args []string 15 | payload []byte 16 | stdout string 17 | stderr string 18 | }{ 19 | {"echo", []string{"test"}, []byte("{}"), "test\n", ""}, 20 | {"sh", []string{"-c", "echo test >&2"}, []byte("{}"), "", "test\n"}, 21 | } 22 | 23 | for i, c := range cases { 24 | fn := NewFunction(c.command, c.args, c.payload) 25 | 26 | rstdout, wstdout, _ := os.Pipe() 27 | rstderr, wstderr, _ := os.Pipe() 28 | 29 | fn.SetStdout(wstdout) 30 | fn.SetStderr(wstderr) 31 | 32 | stdout, stderr, err := fn.Run() 33 | 34 | wstdout.Close() 35 | wstderr.Close() 36 | 37 | if err != nil { 38 | t.Errorf("simple case(%d) failed): %s", i, err) 39 | } 40 | 41 | if stdout != c.stdout { 42 | t.Errorf("simple case(%d) stdout want: %s, got: %s", i, c.stdout, stdout) 43 | } 44 | 45 | rso, _ := io.ReadAll(rstdout) 46 | if string(rso) != c.stdout { 47 | t.Errorf("simple case(%d) rstdout want: %s, got: %s", i, c.stdout, rso) 48 | } 49 | 50 | if stderr != c.stderr { 51 | t.Errorf("simple case(%d) stderr want: %s, got: %s", i, c.stderr, stderr) 52 | } 53 | 54 | rse, _ := io.ReadAll(rstderr) 55 | if string(rse) != c.stderr { 56 | t.Errorf("simple case(%d) rstderr want: %s, got: %s", i, c.stderr, rse) 57 | } 58 | } 59 | } 60 | 61 | func TestRunFuzzing(t *testing.T) { 62 | for i := 1; i <= 10; i++ { 63 | f := fuzz.New() 64 | var str string 65 | f.Fuzz(&str) 66 | 67 | fn := NewFunction("echo", []string{"-n", str}, []byte("{}")) 68 | 69 | fn.SetStdout(io.Discard) 70 | fn.SetStderr(io.Discard) 71 | 72 | stdout, _, _ := fn.Run() 73 | 74 | if stdout != str { 75 | t.Errorf("fuzzing stdout want: %s, got: %s", str, stdout) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lapper 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.48.0 7 | github.com/google/gofuzz v1.2.0 8 | github.com/nlopes/slack v0.6.0 9 | ) 10 | 11 | require ( 12 | github.com/gorilla/websocket v1.4.1 // indirect 13 | github.com/pkg/errors v0.8.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-lambda-go v1.48.0 h1:1aZUYsrJu0yo5fC4z+Rba1KhNImXcJcvHu763BxoyIo= 2 | github.com/aws/aws-lambda-go v1.48.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 6 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 7 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 8 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 9 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 10 | github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= 11 | github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= 12 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 13 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 17 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 18 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /lapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finatext/lapper/f4f8da88bc4ae5f1efdfc2f66913b2b9a4636d93/lapper.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/aws/aws-lambda-go/lambda" 12 | "github.com/aws/aws-lambda-go/lambdacontext" 13 | ) 14 | 15 | func HandleEvent(ctx context.Context, payload json.RawMessage) (string, error) { 16 | lc, _ := lambdacontext.FromContext(ctx) 17 | os.Setenv("AWS_REQUEST_ID", lc.AwsRequestID) 18 | 19 | notifyCond := getEnv("LAPPER_NOTIFY_COND", "stderr") 20 | debug := getEnv("DEBUG", "false") 21 | 22 | slackWebhookURL := os.Getenv("LAPPER_SLACK_WEBHOOK_URL") 23 | handler := strings.Split(os.Getenv("_HANDLER"), " ") 24 | 25 | command := handler[0] 26 | args := handler[1:] 27 | 28 | if debug == "true" { 29 | fmt.Println("[Lapper] Command: ", command, strings.Join(args, " ")) 30 | fmt.Println("[Lapper] Payload: ", string(payload)) 31 | } 32 | 33 | fn := NewFunction(command, args, payload) 34 | stdout, stderr, err := fn.Run() 35 | 36 | if slackWebhookURL != "" && 37 | (notifyCond == "all" || 38 | (notifyCond == "stderr" && stderr != "") || 39 | (notifyCond == "exitcode" && err != nil)) { 40 | 41 | af := buildSlackAttachmentFields(os.Getenv("AWS_LAMBDA_FUNCTION_NAME"), lc.AwsRequestID, stdout, stderr, err) 42 | if err := postSlack(slackWebhookURL, af); err != nil { 43 | return "Failed", fmt.Errorf("Lapper failed to post a message to slack: %w", err) 44 | } 45 | } 46 | 47 | if err != nil { 48 | return "Failed", err 49 | } 50 | 51 | return "Succeeded", nil 52 | } 53 | 54 | func getEnv(key, fallback string) string { 55 | value, exists := os.LookupEnv(key) 56 | if !exists { 57 | value = fallback 58 | } 59 | return value 60 | } 61 | 62 | func main() { 63 | if flag.Lookup("test.v") != nil { 64 | return 65 | } 66 | 67 | lambda.Start(HandleEvent) 68 | } 69 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nlopes/slack" 5 | ) 6 | 7 | func postSlack(url string, afs []slack.AttachmentField) error { 8 | msg := slack.WebhookMessage{} 9 | msg.Username = "Lapper" 10 | msg.IconEmoji = ":mega:" 11 | msg.Text = "Notification from Lapper" 12 | msg.Attachments = []slack.Attachment{ 13 | { 14 | Fields: afs, 15 | }, 16 | } 17 | 18 | return slack.PostWebhook(url, &msg) 19 | } 20 | 21 | func buildSlackAttachmentFields(fName string, rid string, stdout string, stderr string, err error) []slack.AttachmentField { 22 | postStdout := getEnv("LAPPER_POST_STDOUT", "false") 23 | postStderr := getEnv("LAPPER_POST_STDERR", "true") 24 | postError := getEnv("LAPPER_POST_ERROR", "true") 25 | 26 | af := []slack.AttachmentField{} 27 | 28 | af = append(af, slack.AttachmentField{Title: "Function Name", Value: fName, Short: true}) 29 | af = append(af, slack.AttachmentField{Title: "Request Id", Value: rid, Short: true}) 30 | 31 | if postStdout == "true" { 32 | af = append(af, slack.AttachmentField{Title: "STDOUT", Value: stdout, Short: false}) 33 | } 34 | 35 | if postStderr == "true" { 36 | af = append(af, slack.AttachmentField{Title: "STDERR", Value: stderr, Short: false}) 37 | } 38 | 39 | if postError == "true" && err != nil { 40 | af = append(af, slack.AttachmentField{Title: "ERROR", Value: err.Error(), Short: false}) 41 | } 42 | 43 | return af 44 | } 45 | --------------------------------------------------------------------------------