diff --git a/Makefile b/Makefile index d91df0f..dfab0e4 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ MAINTAINER_NAME = Jose Diaz-Gonzalez REPOSITORY = lambda-builder HARDWARE = $(shell uname -m) SYSTEM_NAME = $(shell uname -s | tr '[:upper:]' '[:lower:]') -BASE_VERSION ?= 0.2.0 +BASE_VERSION ?= 0.3.0 IMAGE_NAME ?= $(MAINTAINER)/$(REPOSITORY) PACKAGECLOUD_REPOSITORY ?= dokku/dokku-betafish diff --git a/README.md b/README.md index 3c2beef..161ef10 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ I don't want to go through the motions of figuring out the correct way to build ```shell # substitute the version number as desired -go build -ldflags "-X main.Version=0.2.0 +go build -ldflags "-X main.Version=0.3.0 ``` ## Usage @@ -28,7 +28,7 @@ Available commands are: version Return the version of the binary ``` -### Building an image +### Building an app To build an app: @@ -53,6 +53,12 @@ Custom environment variables can be supplied for the build environment by specif lambda-builder build --build-env KEY=VALUE --build-env ANOTHER_KEY=some-value ``` +A `builder` can be chosen by a flag. Note that while a `builder` may be selected, the detection for that builder must still pass in order for the build to succeed. + +```shell +lambda-builder build --generate-image --builder dotnet +```` + #### Building an image A docker image can be produced from the generated artifact by specifying the `--generate-image` flag. This also allows for multiple `--label` flags as well as specifying a single image tag via either `-t` or `--tag`: @@ -84,6 +90,27 @@ Custom environment variables can be supplied for the built image by specifying o lambda-builder build --generate-image --image-env KEY=VALUE --image-env ANOTHER_KEY=some-value ``` +The `build-image` and `run-image` can also be specified as flags: + +```shell +lambda-builder build --generate-image --build-image "mlupin/docker-lambda:dotnetcore3.1-build" --run-image "mlupin/docker-lambda:dotnetcore3.1" +```` + +A generated image can be run locally with the following line: + +```shell +# run the container and ensure it stays open +# replace `$APP` with your folder name +docker run --rm -it -e DOCKER_LAMBDA_STAY_OPEN=1 -p 9001:9001 "lambda-builder:$APP:latest" + +# invoke it using the awscli (v2) +# note that the function name in this example is `function.handler` +aws lambda invoke --endpoint http://localhost:9001 --no-sign-request --function-name function.handler --payload '{}' --cli-binary-format raw-in-base64-out output.json + +# invoke it via curl +curl -d '{}' http://localhost:9001/2015-03-31/functions/function.handler/invocations +``` + #### Generating a Procfile A `Procfile` can be written to the working directory by specifying the `--write-procfile` flag. This file will not be written if one already exists in the working directory. If an image is being built, the detected handler will also be injected into the build context and used as the default `CMD` for the image. The contents of the `Procfile` are a `web` process type and a detected handler. @@ -112,7 +139,7 @@ Internally, `lambda-builder` detects a given language and builds the app accordi - dotnetcore3.1 - `go` - default build image: `lambci/lambda:build-go1.x` - - requirement: `go.mod` + - requirement: `go.mod` or `main.go` - runtimes: - provided.al2 - `nodejs` diff --git a/builders/go.go b/builders/go.go index 10ffd0f..7b8776a 100644 --- a/builders/go.go +++ b/builders/go.go @@ -24,7 +24,11 @@ func NewGoBuilder(config Config) (GoBuilder, error) { } func (b GoBuilder) Detect() bool { - if io.FileExistsInDirectory(b.Config.WorkingDirectory, "go.sum") { + if io.FileExistsInDirectory(b.Config.WorkingDirectory, "go.mod") { + return true + } + + if io.FileExistsInDirectory(b.Config.WorkingDirectory, "main.go") { return true } @@ -76,8 +80,13 @@ puts-step() { } install-gomod() { - puts-step "Downloading dependencies via go mod" - go mod download 2>&1 | indent + if [[ -f "go.mod" ]]; then + puts-step "Downloading dependencies via go mod" + go mod download 2>&1 | indent + else + puts-step "Missing go.mod, downloading dependencies via go get" + go get + fi puts-step "Compiling via go build" go build -o bootstrap main.go 2>&1 | indent diff --git a/builders/main.go b/builders/main.go index 410725f..d89e861 100644 --- a/builders/main.go +++ b/builders/main.go @@ -26,9 +26,10 @@ type Builder interface { type Config struct { BuildEnv []string - GenerateImage bool + Builder string BuilderBuildImage string BuilderRunImage string + GenerateImage bool Handler string HandlerMap map[string]string Identifier string @@ -37,8 +38,8 @@ type Config struct { ImageTag string Port int RunQuiet bool - WriteProcfile bool WorkingDirectory string + WriteProcfile bool } func (c Config) GetImageTag() string { diff --git a/commands/build.go b/commands/build.go index a85c514..1ff278f 100644 --- a/commands/build.go +++ b/commands/build.go @@ -20,6 +20,8 @@ type BuildCommand struct { command.Meta buildEnv []string + builder string + buildImage string generateImage bool handler string imageEnv []string @@ -27,6 +29,7 @@ type BuildCommand struct { labels []string port int quiet bool + runImage string workingDirectory string writeProcfile bool } @@ -74,9 +77,12 @@ func (c *BuildCommand) FlagSet() *flag.FlagSet { f.BoolVar(&c.quiet, "quiet", false, "run builder in quiet mode") f.BoolVar(&c.writeProcfile, "write-procfile", false, "writes a Procfile if a handler is specified or detected") f.IntVar(&c.port, "port", -1, "set the default port for the lambda to listen on") + f.StringVar(&c.builder, "builder", "", "set the builder to use") + f.StringVar(&c.buildImage, "build-image", "", "set the build-image to use") f.StringVar(&c.handler, "handler", "", "handler override to specify as the default command to run in a built image") - f.StringVarP(&c.imageTag, "tag", "t", "", "name and optionally a tag in the 'name:tag' format") + f.StringVar(&c.runImage, "run-image", "", "set the run-image to use") f.StringVar(&c.workingDirectory, "working-directory", workingDirectory, "working directory") + f.StringVarP(&c.imageTag, "tag", "t", "", "name and optionally a tag in the 'name:tag' format") f.StringArrayVar(&c.buildEnv, "build-env", []string{}, "environment variables to be set for the build context") f.StringArrayVar(&c.imageEnv, "image-env", []string{}, "environment variables to be committed to a built image") f.StringArrayVar(&c.labels, "label", []string{}, "set metadata for an image") @@ -88,12 +94,15 @@ func (c *BuildCommand) AutocompleteFlags() complete.Flags { c.Meta.AutocompleteFlags(command.FlagSetClient), complete.Flags{ "--build-env": complete.PredictAnything, + "--build-image": complete.PredictAnything, + "--builder": complete.PredictSet("dotnet", "go", "nodejs", "python", "ruby"), "--generate-image": complete.PredictNothing, "--handler": complete.PredictAnything, "--image-env": complete.PredictAnything, "--label": complete.PredictAnything, "--port": complete.PredictAnything, "--quiet": complete.PredictNothing, + "--run-image": complete.PredictAnything, "-t": complete.PredictAnything, "--tag": complete.PredictAnything, "--working-directory": complete.PredictAnything, @@ -136,16 +145,19 @@ func (c *BuildCommand) Run(args []string) int { identifier := uuid.New().String() config := builders.Config{ - BuildEnv: c.buildEnv, - GenerateImage: c.generateImage, - Identifier: identifier, - ImageEnv: c.imageEnv, - ImageLabels: c.labels, - ImageTag: c.imageTag, - Port: c.port, - RunQuiet: c.quiet, - WriteProcfile: c.writeProcfile, - WorkingDirectory: c.workingDirectory, + BuildEnv: c.buildEnv, + Builder: c.builder, + BuilderBuildImage: c.buildImage, + BuilderRunImage: c.runImage, + GenerateImage: c.generateImage, + Identifier: identifier, + ImageEnv: c.imageEnv, + ImageLabels: c.labels, + ImageTag: c.imageTag, + Port: c.port, + RunQuiet: c.quiet, + WorkingDirectory: c.workingDirectory, + WriteProcfile: c.writeProcfile, } logger.LogHeader1("Detecting builder") @@ -223,8 +235,12 @@ func detectBuilder(config builders.Config) (builders.Builder, error) { } bs = append(bs, builder) + selectedImage := lambdaYML.Builder + if config.Builder != "" { + selectedImage = config.Builder + } for _, builder := range bs { - if lambdaYML.Builder != "" && lambdaYML.Builder != builder.Name() { + if selectedImage != "" && selectedImage != builder.Name() { continue } diff --git a/test.bats b/test.bats index 43a3b32..4425095 100644 --- a/test.bats +++ b/test.bats @@ -34,6 +34,13 @@ teardown_file() { [[ "$status" -eq 0 ]] } +@test "[build] go without go modules" { + run $LAMBDA_BUILDER_BIN build --working-directory tests/go-nomod + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] +} + @test "[build] hooks" { run $LAMBDA_BUILDER_BIN build --working-directory tests/hooks echo "output: $output" diff --git a/tests/go-nomod/main.go b/tests/go-nomod/main.go new file mode 100644 index 0000000..3dc0f71 --- /dev/null +++ b/tests/go-nomod/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + + "github.com/aws/aws-lambda-go/lambda" +) + +type MyEvent struct { + Name string `json:"name"` +} + +func HandleRequest(ctx context.Context, name MyEvent) (string, error) { + return fmt.Sprintf("Hello %s!", name.Name), nil +} + +func main() { + lambda.Start(HandleRequest) +} diff --git a/tests/go-nomod/test.bats b/tests/go-nomod/test.bats new file mode 100644 index 0000000..f5326d7 --- /dev/null +++ b/tests/go-nomod/test.bats @@ -0,0 +1,61 @@ +#!/usr/bin/env bats + +export LAMBDA_ROLE="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +export AWS_ACCOUNT_ID="$(aws sts get-caller-identity | jq -r ".Account")" +export LAMBDA_FUNCTION_NAME=lambda-go1x +export LAMBDA_RUNTIME=provided.al2 +export LAMBDA_HANDLER=bootstrap + +setup() { + aws lambda delete-function --function-name "$LAMBDA_FUNCTION_NAME" 2>/dev/null || true + aws iam detach-role-policy --role-name "$LAMBDA_FUNCTION_NAME" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2>/dev/null || true + aws iam delete-role --role-name "$LAMBDA_FUNCTION_NAME" 2>/dev/null || true +} + +teardown() { + aws lambda delete-function --function-name "$LAMBDA_FUNCTION_NAME" 2>/dev/null || true + aws iam detach-role-policy --role-name "$LAMBDA_FUNCTION_NAME" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2>/dev/null || true + aws iam delete-role --role-name "$LAMBDA_FUNCTION_NAME" 2>/dev/null || true +} + +@test "aws test" { + run /bin/bash -c "lambda-builder build" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "aws iam create-role --role-name '$LAMBDA_FUNCTION_NAME' --tags 'Key=app,Value=lambda-builder' --tags 'Key=com.dokku.lambda-builder/runtime,Value=$LAMBDA_RUNTIME' --assume-role-policy-document '{\"Version\": \"2012-10-17\", \"Statement\": [{ \"Effect\": \"Allow\", \"Principal\": {\"Service\": \"lambda.amazonaws.com\"}, \"Action\": \"sts:AssumeRole\"}]}'" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "aws iam attach-role-policy --role-name '$LAMBDA_FUNCTION_NAME' --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "sleep 10" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "aws lambda create-function --function-name '$LAMBDA_FUNCTION_NAME' --package-type Zip --tags 'app=lambda-builder,com.dokku.lambda-builder/runtime=$LAMBDA_RUNTIME' --role 'arn:aws:iam::${AWS_ACCOUNT_ID}:role/$LAMBDA_FUNCTION_NAME' --zip-file fileb://lambda.zip --runtime '$LAMBDA_RUNTIME' --handler '$LAMBDA_HANDLER'" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "sleep 10" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "aws lambda get-function --function-name '$LAMBDA_FUNCTION_NAME'" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + run /bin/bash -c "aws lambda invoke --cli-binary-format raw-in-base64-out --function-name '$LAMBDA_FUNCTION_NAME' --payload '{\"name\": \"World\"}' response.json" + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] +} \ No newline at end of file