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 deploy --function-url feature #330

Merged
merged 18 commits into from
Dec 18, 2023
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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,50 @@ Edge functions require two preconditions:

Otherwise, it works as usual.

### Lambda Function URLs support

lambroll can deploy [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html).

`lambroll deploy --function-url=function_url.json` deploys a function URL after the function deploied.

When you want to deploy a public (without authentication) function URL, `function_url.json` is shown below.

```json
{
"Config": {
"AuthType": "NONE"
}
}
```

When you want to deploy a private (requires AWS IAM authentication) function URL, `function_url.json` is shown below.

```json
{
"Config": {
"AuthType": "AWS_IAM"
},
"Permissions": [
{
"Principal": "0123456789012"
},
{
"PrincipalOrgID": "o-123456789",
"Principal": "*"
}
]
}
```

- `Config` maps to [CreateFunctionUrlConfigInput](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/lambda#CreateFunctionUrlConfigInput) in AWS SDK Go v2.
- `Config.AuthType` must be `AWS_IAM` or `NONE`.
- `Config.Qualifer` is optional.
- `Permissions` is optional.
- If `Permissions` is not defined and `AuthType` is `NONE`, `Principal` is set to `*` automatically.
- When `AuthType` is `AWS_IAM`, you must define `Permissions` to specify allowed principals.
- Each elements of `Permissons` maps to [AddPermissionInput](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/lambda#AddPermissionInput) in AWS SDK Go v2.
- `function_url.jsonnet` is also supported like `function.jsonnet`.

## LICENSE

MIT License
Expand Down
23 changes: 23 additions & 0 deletions deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type DeployOption struct {
DryRun bool `help:"dry run" default:"false"`
SkipArchive bool `help:"skip to create zip archive. requires Code.S3Bucket and Code.S3Key in function definition" default:"false"`
KeepVersions int `help:"Number of latest versions to keep. Older versions will be deleted. (Optional value: default 0)." default:"0"`
FunctionURL string `help:"path to function-url definiton" default:""`
SkipFunction bool `help:"skip to deploy a function. deploy function-url only" default:"false"`

ExcludeFileOption
}
Expand Down Expand Up @@ -78,6 +80,22 @@ func (app *App) Deploy(ctx context.Context, opt *DeployOption) error {
return fmt.Errorf("failed to load function: %w", err)
}

deployFunctionURL := func(context.Context) error { return nil }
if opt.FunctionURL != "" {
deployFunctionURL = func(ctx context.Context) error {
fc, err := app.loadFunctionUrl(opt.FunctionURL, *fn.FunctionName)
if err != nil {
return fmt.Errorf("failed to load function url config: %w", err)
}
return app.deployFunctionURL(ctx, fc)
}
}

if opt.SkipFunction {
// skip to deploy a function. deploy function-url only
return deployFunctionURL(ctx)
}

log.Printf("[info] starting deploy function %s", *fn.FunctionName)
if current, err := app.lambda.GetFunction(ctx, &lambda.GetFunctionInput{
FunctionName: fn.FunctionName,
Expand Down Expand Up @@ -183,6 +201,11 @@ func (app *App) Deploy(ctx context.Context, opt *DeployOption) error {
if opt.KeepVersions > 0 { // Ignore zero-value.
return app.deleteVersions(ctx, *fn.FunctionName, opt.KeepVersions)
}

if err := deployFunctionURL(ctx); err != nil {
return err
}

return nil
}

Expand Down
96 changes: 90 additions & 6 deletions diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
Expand All @@ -22,10 +23,11 @@ import (

// DiffOption represents options for Diff()
type DiffOption struct {
Src string `help:"function zip archive or src dir" default:"."`
CodeSha256 bool `help:"diff of code sha256" default:"false"`
Unified bool `help:"unified diff" default:"true" negatable:"" short:"u"`
Qualifier string `help:"compare with" default:"$LATEST"`
Src string `help:"function zip archive or src dir" default:"."`
CodeSha256 bool `help:"diff of code sha256" default:"false"`
Unified bool `help:"unified diff" default:"true" negatable:"" short:"u"`
Qualifier *string `help:"compare with"`
FunctionURL string `help:"path to function-url definiton" default:""`

ExcludeFileOption
}
Expand All @@ -51,7 +53,7 @@ func (app *App) Diff(ctx context.Context, opt *DiffOption) error {
var packageType types.PackageType
if res, err := app.lambda.GetFunction(ctx, &lambda.GetFunctionInput{
FunctionName: &name,
Qualifier: &opt.Qualifier,
Qualifier: opt.Qualifier,
}); err != nil {
return fmt.Errorf("failed to GetFunction %s: %w", name, err)
} else {
Expand All @@ -76,7 +78,7 @@ func (app *App) Diff(ctx context.Context, opt *DiffOption) error {

latestJSON, _ := marshalJSON(latestFunc)
newJSON, _ := marshalJSON(newFunc)
remoteArn := app.functionArn(ctx, name) + ":" + opt.Qualifier
remoteArn := fullQualifiedFunctionName(app.functionArn(ctx, name), opt.Qualifier)

if opt.Unified {
edits := myers.ComputeEdits(span.URIFromPath(remoteArn), string(latestJSON), string(newJSON))
Expand Down Expand Up @@ -116,6 +118,88 @@ func (app *App) Diff(ctx context.Context, opt *DiffOption) error {
}
}

if opt.FunctionURL == "" {
return nil
}

if err := app.diffFunctionURL(ctx, name, opt); err != nil {
return err
}
return nil
}

func (app *App) diffFunctionURL(ctx context.Context, name string, opt *DiffOption) error {
var remote, local *types.FunctionUrlConfig
fqName := fullQualifiedFunctionName(name, opt.Qualifier)

fu, err := app.loadFunctionUrl(opt.FunctionURL, name)
if err != nil {
return fmt.Errorf("failed to load function-url: %w", err)
} else {
fillDefaultValuesFunctionUrlConfig(fu.Config)
local = &types.FunctionUrlConfig{
AuthType: fu.Config.AuthType,
Cors: fu.Config.Cors,
InvokeMode: fu.Config.InvokeMode,
}
}

if res, err := app.lambda.GetFunctionUrlConfig(ctx, &lambda.GetFunctionUrlConfigInput{
FunctionName: &name,
Qualifier: opt.Qualifier,
}); err != nil {
var nfe *types.ResourceNotFoundException
if errors.As(err, &nfe) {
// empty
remote = &types.FunctionUrlConfig{}
} else {
return fmt.Errorf("failed to get function url config: %w", err)
}
} else {
log.Println("[debug] FunctionUrlConfig found")
remote = &types.FunctionUrlConfig{
AuthType: res.AuthType,
Cors: res.Cors,
InvokeMode: res.InvokeMode,
}
}
r, _ := marshalJSON(remote)
l, _ := marshalJSON(local)

if opt.Unified {
edits := myers.ComputeEdits(span.URIFromPath(fqName), string(r), string(l))
if ds := fmt.Sprint(gotextdiff.ToUnified(fqName, opt.FunctionURL, string(r), edits)); ds != "" {
fmt.Print(coloredDiff(ds))
}
} else {
if ds := diff.Diff(string(r), string(l)); ds != "" {
fmt.Println(color.RedString("---" + fqName))
fmt.Println(color.GreenString("+++" + opt.FunctionURL))
fmt.Print(coloredDiff(ds))
}
}

// permissions
adds, removes, err := app.calcFunctionURLPermissionsDiff(ctx, fu)
if err != nil {
return err
}
var addsB []byte
for _, in := range adds {
b, _ := marshalJSON(in)
addsB = append(addsB, b...)
}
var removesB []byte
for _, in := range removes {
b, _ := marshalJSON(in)
removesB = append(removesB, b...)
}
if ds := diff.Diff(string(removesB), string(addsB)); ds != "" {
fmt.Println(color.RedString("---"))
fmt.Println(color.GreenString("+++"))
fmt.Print(coloredDiff(ds))
}

return nil
}

Expand Down
Loading
Loading