Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --symlink to archive symlinks as is. #430

Merged
merged 6 commits into from
Aug 29, 2024
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
13 changes: 8 additions & 5 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ jobs:
strategy:
matrix:
go:
- "1.20"
- "1.21"
- "1.22"
- "1.23"
name: Build
runs-on: ubuntu-latest

Expand All @@ -23,15 +24,17 @@ jobs:
- 4566:4566

steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
cache: true
cache-dependency-path: go.sum
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v3

- name: Build & Test
run: |
echo $PATH
Expand Down
33 changes: 0 additions & 33 deletions .github/workflows/manual.yml

This file was deleted.

8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21"
go-version: "1.23"
check-latest: true

- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ lambroll is a simple deployment tool for [AWS Lambda](https://aws.amazon.com/lam
lambroll does,

- Create a function.
- Create a Zip archive from local directory.
- Create a Zip archive from a local directory.
- Deploy function code / configuration / tags / aliases / function URLs.
- Rollback a function to previous version.
- Rollback a function to the previous version.
- Invoke a function with payloads.
- Manage function versions.
- Show status of a function.
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
- uses: actions/checkout@v4
- uses: fujiwara/lambroll@v1
with:
version: v1.0.4
version: v1.1.0
- run: |
lambroll deploy
```
Expand Down Expand Up @@ -228,13 +228,13 @@ Flags:
--alias="current" alias name for publish
--alias-to-latest set alias to unpublished $LATEST version
--dry-run dry run
--skip-archive skip to create zip archive. requires Code.S3Bucket and Code.S3Key in function
definition
--keep-versions=0 Number of latest versions to keep. Older versions will be deleted. (Optional
value: default 0).
--function-url="" path to function-url definition
--skip-archive skip to create zip archive. requires Code.S3Bucket and Code.S3Key in function definition
--keep-versions=0 Number of latest versions to keep. Older versions will be deleted. (Optional value: default 0).
--ignore="" ignore fields by jq queries in function.json
--function-url="" path to function-url definition ($LAMBROLL_FUNCTION_URL)
--skip-function skip to deploy a function. deploy function-url only
--exclude-file=".lambdaignore" exclude file
--symlink keep symlink (same as zip --symlink,-y)
```

`deploy` works as below.
Expand Down
78 changes: 53 additions & 25 deletions archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"context"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -19,7 +21,7 @@ type ArchiveOption struct {
Src string `help:"function zip archive or src dir" default:"."`
Dest string `help:"destination file path" default:"function.zip"`

ExcludeFileOption
ZipOption
}

// Archive archives zip
Expand All @@ -28,7 +30,7 @@ func (app *App) Archive(ctx context.Context, opt *ArchiveOption) error {
return err
}

zipfile, _, err := createZipArchive(opt.Src, opt.excludes)
zipfile, _, err := createZipArchive(opt.Src, opt.excludes, opt.KeepSymlink)
if err != nil {
return err
}
Expand Down Expand Up @@ -75,14 +77,14 @@ func loadZipArchive(src string) (*os.File, os.FileInfo, error) {
}

// createZipArchive creates a zip archive
func createZipArchive(src string, excludes []string) (*os.File, os.FileInfo, error) {
func createZipArchive(src string, excludes []string, keepSymlink bool) (*os.File, os.FileInfo, error) {
log.Printf("[info] creating zip archive from %s", src)
tmpfile, err := os.CreateTemp("", "archive")
if err != nil {
return nil, nil, fmt.Errorf("failed to open tempFile: %w", err)
}
w := zip.NewWriter(tmpfile)
err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
err = filepath.WalkDir(src, func(path string, info fs.DirEntry, err error) error {
log.Println("[trace] waking", path)
if err != nil {
log.Println("[error] failed to walking dir in", src)
Expand All @@ -97,12 +99,12 @@ func createZipArchive(src string, excludes []string) (*os.File, os.FileInfo, err
return nil
}
log.Println("[trace] adding", relpath)
return addToZip(w, path, relpath, info)
return addToZip(w, path, relpath, info, keepSymlink)
})
if err := w.Close(); err != nil {
return nil, nil, fmt.Errorf("failed to create zip archive: %w", err)
}
tmpfile.Seek(0, os.SEEK_SET)
tmpfile.Seek(0, io.SeekStart)
stat, _ := tmpfile.Stat()
log.Printf("[info] zip archive wrote %d bytes", stat.Size())
return tmpfile, stat, err
Expand All @@ -117,23 +119,55 @@ func matchExcludes(path string, excludes []string) bool {
return false
}

func addToZip(z *zip.Writer, path, relpath string, info os.FileInfo) error {
// treat symlink as file
if info.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
log.Printf("[error] failed to read symlink %s: %s", path, err)
return err
func followSymlink(path string) (string, fs.FileInfo, error) {
link, err := os.Readlink(path)
if err != nil {
return "", nil, fmt.Errorf("failed to read symlink %s: %s", path, err)
}
linkTarget := filepath.Join(filepath.Dir(path), link)
log.Printf("[debug] resolve symlink %s to %s", path, linkTarget)
info, err := os.Stat(linkTarget)
if err != nil {
return "", nil, fmt.Errorf("failed to stat symlink target %s: %s", linkTarget, err)
}
if info.IsDir() {
return "", nil, fmt.Errorf("symlink target is a directory %s", linkTarget)
}
return linkTarget, info, nil
}

func addToZip(z *zip.Writer, path, relpath string, entry fs.DirEntry, keepSymlink bool) error {
info, err := entry.Info()
if err != nil {
log.Printf("[error] failed to get info %s: %s", path, err)
return err
}
var reader io.ReadCloser
if info.Mode()&fs.ModeSymlink != 0 { // is symlink
if keepSymlink {
link, err := os.Readlink(path)
if err != nil {
return fmt.Errorf("failed to read symlink %s: %w", path, err)
}
reader = io.NopCloser(strings.NewReader(link))
} else {
// treat symlink as file. skip symlink target directory.
path, info, err = followSymlink(path) // overwrite path, info
if err != nil {
log.Printf("[warn] failed to follow symlink. skip: %s", err)
return nil
}
}
linkTarget := filepath.Join(filepath.Dir(path), link)
log.Printf("[debug] resolve symlink %s to %s", path, linkTarget)
info, err = os.Stat(linkTarget)
}
if reader == nil {
reader, err = os.Open(path)
if err != nil {
log.Printf("[error] failed to stat symlink target %s: %s", linkTarget, err)
log.Printf("[error] failed to open %s: %s", path, err)
return err
}
path = linkTarget
}
defer reader.Close()

header, err := zip.FileInfoHeader(info)
if err != nil {
log.Println("[error] failed to create zip file header", err)
Expand All @@ -146,13 +180,7 @@ func addToZip(z *zip.Writer, path, relpath string, info os.FileInfo) error {
log.Println("[error] failed to create in zip", err)
return err
}
r, err := os.Open(path)
if err != nil {
log.Printf("[error] failed to open %s: %s", path, err)
return err
}
defer r.Close()
_, err = io.Copy(w, r)
_, err = io.Copy(w, reader)
log.Printf("[debug] %s %10d %s %s",
header.Mode(),
header.UncompressedSize64,
Expand Down
42 changes: 35 additions & 7 deletions archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,44 @@ import (
"archive/zip"
"fmt"
"os"
"slices"
"testing"
"time"

"github.com/fujiwara/lambroll"
"github.com/google/go-cmp/cmp"
)

type zipTestSuite struct {
WorkingDir string
SrcDir string
WorkingDir string
SrcDir string
Expected []string
KeepSymlink bool
}

func (s zipTestSuite) String() string {
return fmt.Sprintf("%s_src_%s", s.WorkingDir, s.SrcDir)
}

var createZipArchives = []zipTestSuite{
{".", "test/src"},
{"test/src/dir", "../"},
{
WorkingDir: ".",
SrcDir: "test/src",
Expected: []string{"dir/sub.txt", "ext-hello.txt", "hello.symlink", "hello.txt", "index.js", "world"},
KeepSymlink: false,
},
{
WorkingDir: "test/src/dir",
SrcDir: "../",
Expected: []string{"dir/sub.txt", "ext-hello.txt", "hello.symlink", "hello.txt", "index.js", "world"},
KeepSymlink: false,
},
{
WorkingDir: ".",
SrcDir: "test/src",
Expected: []string{"dir/sub.txt", "dir.symlink", "ext-hello.txt", "hello.symlink", "hello.txt", "index.js", "world"},
KeepSymlink: true,
},
}

func TestCreateZipArchive(t *testing.T) {
Expand All @@ -40,7 +60,7 @@ func testCreateZipArchive(t *testing.T, s zipTestSuite) {
excludes := []string{}
excludes = append(excludes, lambroll.DefaultExcludes...)
excludes = append(excludes, []string{"*.bin", "skip/*"}...)
r, info, err := lambroll.CreateZipArchive(s.SrcDir, excludes)
r, info, err := lambroll.CreateZipArchive(s.SrcDir, excludes, s.KeepSymlink)
if err != nil {
t.Error("failed to CreateZipArchive", err)
}
Expand All @@ -51,9 +71,10 @@ func testCreateZipArchive(t *testing.T, s zipTestSuite) {
if err != nil {
t.Error("failed to new zip reader", err)
}
if len(zr.File) != 6 {
t.Errorf("unexpected included files num %d expect %d", len(zr.File), 6)
if len(zr.File) != len(s.Expected) {
t.Errorf("unexpected included files num %d expect %d", len(zr.File), len(s.Expected))
}
zipFiles := []string{}
for _, f := range zr.File {
h := f.FileHeader
t.Logf("%s %10d %s %s",
Expand All @@ -62,7 +83,14 @@ func testCreateZipArchive(t *testing.T, s zipTestSuite) {
h.Modified.Format(time.RFC3339),
h.Name,
)
zipFiles = append(zipFiles, h.Name)
}
slices.Sort(zipFiles)
slices.Sort(s.Expected)
if diff := cmp.Diff(zipFiles, s.Expected); diff != "" {
t.Errorf("unexpected included files %s", diff)
}

if info.Size() < 100 {
t.Errorf("too small file got %d bytes", info.Size())
}
Expand Down
6 changes: 3 additions & 3 deletions create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import (

var directUploadThreshold = int64(50 * 1024 * 1024) // 50MB

func prepareZipfile(src string, excludes []string) (*os.File, os.FileInfo, error) {
func prepareZipfile(src string, excludes []string, keepSymlink bool) (*os.File, os.FileInfo, error) {
if fi, err := os.Stat(src); err != nil {
return nil, nil, fmt.Errorf("src %s is not found: %w", src, err)
} else if fi.IsDir() {
zipfile, info, err := createZipArchive(src, excludes)
zipfile, info, err := createZipArchive(src, excludes, keepSymlink)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -54,7 +54,7 @@ func (app *App) prepareFunctionCodeForDeploy(ctx context.Context, opt *DeployOpt
return nil
}

zipfile, info, err := prepareZipfile(opt.Src, opt.excludes)
zipfile, info, err := prepareZipfile(opt.Src, opt.excludes, opt.KeepSymlink)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type DeployOption struct {
FunctionURL string `help:"path to function-url definition" default:"" env:"LAMBROLL_FUNCTION_URL"`
SkipFunction bool `help:"skip to deploy a function. deploy function-url only" default:"false"`

ExcludeFileOption
ZipOption
}

func (opt DeployOption) label() string {
Expand Down
Loading