diff --git a/cmd/build-lambda-zip/main.go b/cmd/build-lambda-zip/main.go index ef372ee6..e79320ae 100644 --- a/cmd/build-lambda-zip/main.go +++ b/cmd/build-lambda-zip/main.go @@ -1,3 +1,5 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved + package main import ( @@ -5,6 +7,7 @@ import ( "errors" "fmt" "io/ioutil" + "log" "os" "path/filepath" @@ -37,6 +40,7 @@ func main() { if err := compressExeAndArgs(outputZip, inputExe, c.Args().Tail()); err != nil { return fmt.Errorf("failed to compress file: %v", err) } + log.Print("wrote " + outputZip) return nil }, } @@ -48,6 +52,18 @@ func main() { } func writeExe(writer *zip.Writer, pathInZip string, data []byte) error { + if pathInZip != "bootstrap" { + header := &zip.FileHeader{Name: "bootstrap", Method: zip.Deflate} + header.SetMode(0755 | os.ModeSymlink) + link, err := writer.CreateHeader(header) + if err != nil { + return err + } + if _, err := link.Write([]byte(pathInZip)); err != nil { + return err + } + } + exe, err := writer.CreateHeader(&zip.FileHeader{ CreatorVersion: 3 << 8, // indicates Unix ExternalAttrs: 0777 << 16, // -rwxrwxrwx file permissions diff --git a/cmd/build-lambda-zip/main_test.go b/cmd/build-lambda-zip/main_test.go new file mode 100644 index 00000000..0e1571fd --- /dev/null +++ b/cmd/build-lambda-zip/main_test.go @@ -0,0 +1,159 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved + +package main + +import ( + "archive/zip" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSizes(t *testing.T) { + if testing.Short() { + t.Skip() + return + } + t.Log("test how different arguments affect binary and archive sizes") + cases := []struct { + file string + args []string + }{ + {"testdata/apigw.go", nil}, + {"testdata/noop.go", nil}, + {"testdata/noop.go", []string{"-tags", "lambda.norpc"}}, + {"testdata/noop.go", []string{"-ldflags=-s -w"}}, + {"testdata/noop.go", []string{"-tags", "lambda.norpc", "-ldflags=-s -w"}}, + } + testDir, err := os.Getwd() + require.NoError(t, err) + tempDir, err := ioutil.TempDir("/tmp", "build-lambda-zip") + require.NoError(t, err) + for _, test := range cases { + require.NoError(t, os.Chdir(testDir)) + testName := fmt.Sprintf("%s, %v", test.file, test.args) + t.Run(testName, func(t *testing.T) { + binPath := path.Join(tempDir, test.file+".bin") + zipPath := path.Join(tempDir, test.file+".zip") + + buildArgs := []string{"build", "-o", binPath} + buildArgs = append(buildArgs, test.args...) + buildArgs = append(buildArgs, test.file) + + gocmd := exec.Command("go", buildArgs...) + gocmd.Env = append(os.Environ(), "GOOS=linux") + gocmd.Stderr = os.Stderr + require.NoError(t, gocmd.Run()) + require.NoError(t, os.Chdir(filepath.Dir(binPath))) + require.NoError(t, compressExeAndArgs(zipPath, binPath, []string{})) + + binInfo, err := os.Stat(binPath) + require.NoError(t, err) + zipInfo, err := os.Stat(zipPath) + require.NoError(t, err) + + t.Logf("zip size = %d Kb, bin size = %d Kb", zipInfo.Size()/1024, binInfo.Size()/1024) + }) + } + +} + +func TestCompressExeAndArgs(t *testing.T) { + tempDir, err := ioutil.TempDir("/tmp", "build-lambda-zip") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + fileNames := []string{"the-exe", "the-config", "other-config"} + filePaths := make([]string, len(fileNames)) + for i, fileName := range fileNames { + filePaths[i] = filepath.Join(tempDir, fileName) + f, err := os.Create(filePaths[i]) + require.NoError(t, err) + _, _ = fmt.Fprintf(f, "Hello file %d!", i) + err = f.Close() + require.NoError(t, err) + } + outZipPath := filepath.Join(tempDir, "lambda.zip") + + err = compressExeAndArgs(outZipPath, filePaths[0], filePaths[1:]) + require.NoError(t, err) + + t.Run("handler exe configured in zip root", func(t *testing.T) { + require.NotEqual(t, filePaths[0], filepath.Base(filePaths[0]), "test precondition") + zipReader, err := zip.OpenReader(outZipPath) + require.NoError(t, err) + defer zipReader.Close() + for _, zf := range zipReader.File { + if zf.Name == filepath.Base(filePaths[0]) { + assert.True(t, zf.FileInfo().Mode().IsRegular()) + permissions := int(zf.FileInfo().Mode() & 0777) + assert.Equal(t, 0111, permissions&0111, "file permissions: %#o, aren't executable", permissions) + assert.Equal(t, 0444, permissions&0444, "file permissions: %#o, aren't readable", permissions) + return + } + } + t.Fatalf("failed to find handler exe in zip") + }) + + t.Run("boostrap is a symlink to handler exe", func(t *testing.T) { + zipReader, err := zip.OpenReader(outZipPath) + require.NoError(t, err) + defer zipReader.Close() + var bootstrap *zip.File + for _, f := range zipReader.File { + if f.Name == "bootstrap" { + bootstrap = f + } + } + require.NotNil(t, bootstrap) + assert.Equal(t, 0755|os.ModeSymlink, bootstrap.FileInfo().Mode()) + link, err := bootstrap.Open() + require.NoError(t, err) + defer link.Close() + linkTarget, err := ioutil.ReadAll(link) + require.NoError(t, err) + assert.Equal(t, filepath.Base(filePaths[0]), string(linkTarget)) + }) + + t.Run("resource file paths", func(t *testing.T) { + zipReader, err := zip.OpenReader(outZipPath) + require.NoError(t, err) + defer zipReader.Close() + eachFile: + for _, path := range filePaths[1:] { + for _, zipFileEntry := range zipReader.File { + if zipFileEntry.Name == path { + continue eachFile + } + } + t.Logf("failed to find resource file %s in zip", path) + t.Fail() + } + }) + + t.Run("file contents match", func(t *testing.T) { + zipReader, err := zip.OpenReader(outZipPath) + require.NoError(t, err) + defer zipReader.Close() + expectedIndex := 0 + for _, zf := range zipReader.File { + if zf.FileInfo().Mode().IsRegular() { + f, err := zf.Open() + require.NoError(t, err) + defer f.Close() + content, err := ioutil.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("Hello file %d!", expectedIndex), string(content), "in file: %s", zf.Name) + expectedIndex++ + } + } + }) + +} diff --git a/cmd/build-lambda-zip/testdata/apigw.go b/cmd/build-lambda-zip/testdata/apigw.go new file mode 100644 index 00000000..31e95f19 --- /dev/null +++ b/cmd/build-lambda-zip/testdata/apigw.go @@ -0,0 +1,18 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved + +package main + +import ( + "context" + "fmt" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func main () { + lambda.Start(func (ctx context.Context, event events.APIGatewayV2HTTPRequest) (*events.APIGatewayProxyResponse, error) { + return &events.APIGatewayProxyResponse{ + Body: fmt.Sprintf("Hello %v", event), + }, nil + }) +} diff --git a/cmd/build-lambda-zip/testdata/noop.go b/cmd/build-lambda-zip/testdata/noop.go new file mode 100644 index 00000000..23dde4af --- /dev/null +++ b/cmd/build-lambda-zip/testdata/noop.go @@ -0,0 +1,21 @@ +// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved + +package main + +import ( + "context" + "github.com/aws/aws-lambda-go/lambda" +) + +type BinaryHandler func(context.Context, []byte) ([]byte, error) +func (bh BinaryHandler) Invoke(ctx context.Context, req []byte) ([]byte, error) { + return bh(ctx, req) +} + +func noop (ctx context.Context, req []byte) ([]byte, error) { + return req, nil +} + +func main() { + lambda.StartHandler(BinaryHandler(noop)) +}