Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ Alternatively, a given path can be specified via the `--working-directory` flag:
lambda-builder build --working-directory path/to/app
```

In addition to the `lambda.yml`, a docker image can be produced from the generated artifact by specifying the `--build-image` flag. This also allows for multiple `--label` flags as well as specifying a single image tag via either `-t` or `--tag`:

```shell
# will write a lambda.zip in the specified path
# and generate a docker image named `lambda-builder:$APP:latest`
# where $APP is the last portion of the working directory
lambda-builder build --build-image

# adds the labels com.example/key=value and com.example/another-key=value
lambda-builder build --build-image --label com.example/key=value --label com.example/another-key=value

# tags the image as app/awesome:1234
lambda-builder build --build-image --tag app/awesome:1234
```

### How does it work

Internally, `lambda-builder` detects a given language and builds the app according to the script specified by the detected builder within a disposablecontainer environment emulating AWS Lambda. If a builder is not detected, the build will fail. The following languages are supported:
Expand Down Expand Up @@ -80,16 +95,22 @@ Internally, `lambda-builder` detects a given language and builds the app accordi

When the app is built, a `lambda.zip` will be produced in the specified working directory. The resulting `lambda.zip` can be uploaded to S3 and used within a Lambda function.

Both the builder and the build image environment can be overriden in an optional `lambda.yml` file in the specified working directory. An example of this file is as follows:
Both the builder and the build image environment can be overriden in an optional `lambda.yml` file in the specified working directory.

### `lambda.yml`

The following a short description of the `lambda.yml` format.

```yaml
---
build_image: mlupin/docker-lambda:dotnetcore3.1-build
builder: dotnet
run_image: mlupin/docker-lambda:dotnetcore3.1
```

- `build_image`: A docker image that is accessible by the docker daemon. The `build_image` _should_ be based on an existing Lambda image - builders may fail if they cannot run within the specified `build_image`. The build will fail if the image is inaccessible by the docker daemon.
- `builder`: The name of a builder. This may be used if multiple builders match and a specific builder is desired. If an invalid builder is specified, the build will fail.
- `run_image`: A docker image that is accessible by the docker daemon. The `run_image` _should_ be based on an existing Lambda image - built images may fail to start if they are not compatible with the produced artifact. The generation of the `run` iage will fail if the image is inaccessible by the docker daemon.

### Deploying

Expand Down
23 changes: 16 additions & 7 deletions builders/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ type DotnetBuilder struct {

func NewDotnetBuilder(config Config) (DotnetBuilder, error) {
var err error
config.BuildImage, err = getBuilder(config, "mlupin/docker-lambda:dotnet6-build")
config.BuilderBuildImage, err = getBuildImage(config, "mlupin/docker-lambda:dotnet6-build")
if err != nil {
return DotnetBuilder{}, err
}

config.BuilderRunImage, err = getRunImage(config, "mlupin/docker-lambda:dotnet6")
if err != nil {
return DotnetBuilder{}, err
}
Expand All @@ -19,11 +24,7 @@ func NewDotnetBuilder(config Config) (DotnetBuilder, error) {
}

func (b DotnetBuilder) BuildImage() string {
return b.Config.BuildImage
}

func (b DotnetBuilder) GetConfig() Config {
return b.Config
return b.Config.BuilderBuildImage
}

func (b DotnetBuilder) Detect() bool {
Expand All @@ -35,7 +36,15 @@ func (b DotnetBuilder) Detect() bool {
}

func (b DotnetBuilder) Execute() error {
return executeBuilder(b.script(), b.Config)
return executeBuilder(b.script(), b.GetTaskBuildDir(), b.Config)
}

func (b DotnetBuilder) GetConfig() Config {
return b.Config
}

func (b DotnetBuilder) GetTaskBuildDir() string {
return "/var/task"
}

func (b DotnetBuilder) Name() string {
Expand Down
23 changes: 16 additions & 7 deletions builders/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ type GoBuilder struct {

func NewGoBuilder(config Config) (GoBuilder, error) {
var err error
config.BuildImage, err = getBuilder(config, "lambci/lambda:build-go1.x")
config.BuilderBuildImage, err = getBuildImage(config, "lambci/lambda:build-go1.x")
if err != nil {
return GoBuilder{}, err
}

config.BuilderRunImage, err = getRunImage(config, "mlupin/docker-lambda:provided.al2")
if err != nil {
return GoBuilder{}, err
}
Expand All @@ -19,11 +24,7 @@ func NewGoBuilder(config Config) (GoBuilder, error) {
}

func (b GoBuilder) BuildImage() string {
return b.Config.BuildImage
}

func (b GoBuilder) GetConfig() Config {
return b.Config
return b.Config.BuilderBuildImage
}

func (b GoBuilder) Detect() bool {
Expand All @@ -35,7 +36,15 @@ func (b GoBuilder) Detect() bool {
}

func (b GoBuilder) Execute() error {
return executeBuilder(b.script(), b.Config)
return executeBuilder(b.script(), b.GetTaskBuildDir(), b.Config)
}

func (b GoBuilder) GetConfig() Config {
return b.Config
}

func (b GoBuilder) GetTaskBuildDir() string {
return "/go/src/handler"
}

func (b GoBuilder) Name() string {
Expand Down
146 changes: 135 additions & 11 deletions builders/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package builders

import (
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -14,25 +15,70 @@ import (

type Builder interface {
GetConfig() Config
GetTaskBuildDir() string
Detect() bool
Execute() error
BuildImage() string
Name() string
}

type Config struct {
BuildImage string
Identifier string
RunQuiet bool
WorkingDirectory string
BuildImage bool
BuilderBuildImage string
BuilderRunImage string
Identifier string
ImageLabels []string
ImageTag string
RunQuiet bool
WorkingDirectory string
}

func (c Config) GetImageTag() string {
if c.ImageTag != "" {
return c.ImageTag
}

appName := filepath.Base(c.WorkingDirectory)
return fmt.Sprintf("lambda-builder/%s:latest", appName)
}

type LambdaYML struct {
Builder string `yaml:"builder"`
BuildImage string `yaml:"build_image"`
RunImage string `yaml:"run_image"`
}

func executeBuilder(script string, taskBuildDir string, config Config) error {
tmp, err := os.MkdirTemp("", "lambda-builder")
defer func() {
os.RemoveAll(tmp)
}()

if err != nil {
return fmt.Errorf("error preparing temporary build directory: %s", err.Error())
}

if err := executeBuildContainer(tmp, script, taskBuildDir, config); err != nil {
return err
}

if config.BuildImage {
fmt.Printf("=====> Building image\n")
fmt.Printf(" Generating temporary Dockerfile\n")
if err := generateDockerfile(tmp, config); err != nil {
return err
}

fmt.Printf(" Executing build of %s\n", config.GetImageTag())
if err := buildDockerImage(tmp, config); err != nil {
return err
}
}

return nil
}

func executeBuilder(script string, config Config) error {
func executeBuildContainer(tmp string, script string, taskBuildDir string, config Config) error {
args := []string{
"container",
"run",
Expand All @@ -41,7 +87,8 @@ func executeBuilder(script string, config Config) error {
"--label", "com.dokku.lambda-builder/executor=true",
"--name", fmt.Sprintf("lambda-builder-executor-%s", config.Identifier),
"--volume", fmt.Sprintf("%s:/tmp/task", config.WorkingDirectory),
config.BuildImage,
"--volume", fmt.Sprintf("%s:%s", tmp, taskBuildDir),
config.BuilderBuildImage,
"/bin/bash", "-c", script,
}

Expand All @@ -54,11 +101,71 @@ func executeBuilder(script string, config Config) error {

res, err := cmd.Execute()
if err != nil {
return fmt.Errorf("failed to execute builder: %s", err.Error())
return fmt.Errorf("error executing builder: %s", err.Error())
}

if res.ExitCode != 0 {
return fmt.Errorf("failed to execute builder, exit code %d", res.ExitCode)
return fmt.Errorf("error executing builder, exit code %d", res.ExitCode)
}

return nil
}

func generateDockerfile(tmp string, config Config) error {
dockerfileName := filepath.Join(tmp, fmt.Sprintf("%s.Dockerfile", config.Identifier))
f, err := os.Create(dockerfileName)
if err != nil {
return fmt.Errorf("error creating Dockerfile: %s", err)
}

tpl, err := template.New("t1").Parse(`
FROM {{ .run_image }}
COPY . /var/task
`)
if err != nil {
return fmt.Errorf("error generating template: %s", err)
}

data := map[string]string{
"run_image": config.BuilderRunImage,
}

if err := tpl.Execute(f, data); err != nil {
return fmt.Errorf("error writing Dockerfile: %s", err)
}

return nil
}

func buildDockerImage(tmp string, config Config) error {
args := []string{
"image",
"build",
"--file", filepath.Join(tmp, fmt.Sprintf("%s.Dockerfile", config.Identifier)),
"--progress", "plain",
"--tag", config.GetImageTag(),
}

for _, label := range config.ImageLabels {
args = append(args, "--label", label)
}

args = append(args, tmp)

cmd := execute.ExecTask{
Args: args,
Command: "docker",
Cwd: config.WorkingDirectory,
StreamStdio: !config.RunQuiet,
}

res, err := cmd.Execute()
if err != nil {
return fmt.Errorf("error building image: %s", err.Error())
}

if res.ExitCode != 0 {
return fmt.Errorf("error building image, exit code %d", res.ExitCode)
}

return nil
Expand Down Expand Up @@ -88,9 +195,9 @@ func ParseLambdaYML(config Config) (LambdaYML, error) {
return lambdaYML, nil
}

func getBuilder(config Config, defaultImage string) (string, error) {
if config.BuildImage != "" {
return config.BuildImage, nil
func getBuildImage(config Config, defaultImage string) (string, error) {
if config.BuilderBuildImage != "" {
return config.BuilderBuildImage, nil
}

lambdaYML, err := ParseLambdaYML(config)
Expand All @@ -104,3 +211,20 @@ func getBuilder(config Config, defaultImage string) (string, error) {

return lambdaYML.BuildImage, nil
}

func getRunImage(config Config, defaultImage string) (string, error) {
if config.BuilderRunImage != "" {
return config.BuilderRunImage, nil
}

lambdaYML, err := ParseLambdaYML(config)
if err != nil {
return "", err
}

if lambdaYML.RunImage == "" {
return defaultImage, nil
}

return lambdaYML.RunImage, nil
}
23 changes: 16 additions & 7 deletions builders/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ type NodejsBuilder struct {

func NewNodejsBuilder(config Config) (NodejsBuilder, error) {
var err error
config.BuildImage, err = getBuilder(config, "mlupin/docker-lambda:nodejs14.x-build")
config.BuilderBuildImage, err = getBuildImage(config, "mlupin/docker-lambda:nodejs14.x-build")
if err != nil {
return NodejsBuilder{}, err
}

config.BuilderRunImage, err = getRunImage(config, "mlupin/docker-lambda:nodejs14.x")
if err != nil {
return NodejsBuilder{}, err
}
Expand All @@ -19,11 +24,7 @@ func NewNodejsBuilder(config Config) (NodejsBuilder, error) {
}

func (b NodejsBuilder) BuildImage() string {
return b.Config.BuildImage
}

func (b NodejsBuilder) GetConfig() Config {
return b.Config
return b.Config.BuilderBuildImage
}

func (b NodejsBuilder) Detect() bool {
Expand All @@ -35,7 +36,15 @@ func (b NodejsBuilder) Detect() bool {
}

func (b NodejsBuilder) Execute() error {
return executeBuilder(b.script(), b.Config)
return executeBuilder(b.script(), b.GetTaskBuildDir(), b.Config)
}

func (b NodejsBuilder) GetConfig() Config {
return b.Config
}

func (b NodejsBuilder) GetTaskBuildDir() string {
return "/var/task"
}

func (b NodejsBuilder) Name() string {
Expand Down
Loading