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 Jsonnet native functions. #429

Merged
merged 9 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
157 changes: 133 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
steps:
- checkout
- lambroll/install:
version: v1.0.1
version: v1.1.0
- run:
command: |
lambroll deploy
Expand Down Expand Up @@ -398,6 +398,11 @@ function.json is a definition for Lambda function. JSON structure is based from
}
}
```

The template functions is available in `{{ }}`.
- `env` function expands environment variables.
- `must_env` function expands environment variables. If the environment variable is not defined, lambroll will panic and abort.

#### Tags

When "Tags" key exists in function.json, lambroll set / remove tags to the lambda function at deploy.
Expand All @@ -415,6 +420,44 @@ When "Tags" key exists in function.json, lambroll set / remove tags to the lambd
When "Tags" key does not exist, lambroll doesn't manage tags.
If you hope to remove all tags, set `"Tags": {}` expressly.

#### Environment variables from envfile

`lambroll --envfile .env1 .env2` reads files named .env1 and .env2 as environment files and export variables in these files.

These files are parsed by [hashicorp/go-envparse](https://github.com/hashicorp/go-envparse).

```env
FOO=foo
export BAR="bar"
```

#### Jsonnet support for function configuration

lambroll also can read function.jsonnet as [Jsonnet](https://jsonnet.org/) format instead of plain JSON.

```jsonnet
{
FunctionName: 'hello',
Handler: 'index.handler',
MemorySize: std.extVar('memorySize'),
Role: 'arn:aws:iam::%s:role/lambda_role' % [ std.extVar('accountID') ],
Runtime: 'nodejs20.x',
}
```

```console
$ lambroll \
--function function.jsonnet \
--ext-str accountID=0123456789012 \
--ext-code memorySize="128 * 4" \
deploy
```

- `--ext-str` sets external string values for Jsonnet.
- `--ext-code` sets external code values for Jsonnet.

v1.1.0 and later, lambroll supports Jsonnet native functions. See below for details.

#### Expand SSM parameter values

At reading the file, lambroll evaluates `{{ ssm }}` syntax in JSON.
Expand All @@ -427,6 +470,19 @@ For example,

SSM parameter value of `/path/to/param` is expanded here.

For Jsonnet, the `ssm` function is available.

```jsonnet
local ssm = std.native('ssm');
{
Environment: {
Variables: {
FOO: ssm('/path/to/param'),
},
},
}
```

#### Expand environment variables

At reading the file, lambroll evaluates `{{ env }}` and `{{ must_env }}` syntax in JSON.
Expand Down Expand Up @@ -457,17 +513,48 @@ Environment variable `FOO` is expanded. When `FOO` is not defined, lambroll will
}
```

#### Environment variables from envfile
For Jsonnet, the `env` and `must_env` native functions are available.

`lambroll --envfile .env1 .env2` reads files named .env1 and .env2 as environment files and export variables in these files.
```jsonnet
local env = std.native('env');
local must_env = std.native('must_env');
{
Environment: {
Variables: {
FOO: env('FOO', 'default for FOO'),
BAR: must_env('BAR'),
},
},
}
```

These files are parsed by [hashicorp/go-envparse](https://github.com/hashicorp/go-envparse).
#### Resolve AWS caller identity

```env
FOO=foo
export BAR="bar"
The `caller_identity` template function resolves the AWS caller identity.

```json
{
"Account": "{{ caller_identity.Account }}",
"Arn": "{{ caller_identity.Arn }}",
"UserId": "{{ caller_identity.UserId }}"
}
```

The `caller_identity` native function also available in Jsonnet.

```jsonnet
local caller = std.native('caller_identity')();
{
Account: caller.Account,
Arn: caller.Arn,
UserId: caller.UserId,
}
```

The `caller_identity` function returns an object containing the following fields: `Account`, `Arn`, and `UserId`.

This object is the same as the result of [GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) API.

#### Lookup resource attributes in tfstate ([Terraform state](https://www.terraform.io/docs/state/index.html))

When `--tfstate` option set to an URL to `terraform.tfstate`, tfstate template function enabled.
Expand All @@ -491,7 +578,7 @@ data "aws_iam_role" "lambda" {
"Handler": "index.js",
"MemorySize": 128,
"Role": "{{ tfstate `data.aws_iam_role.lambda.arn` }}",
"Runtime": "nodejs12.x",
"Runtime": "nodejs20.x",
"Timeout": 5,
"TracingConfig": {
"Mode": "PassThrough"
Expand All @@ -508,6 +595,33 @@ data "aws_iam_role" "lambda" {
}
```

For Jsonnet, the `tfstate` native function is available.

```jsonnet
local tfstate = std.native('tfstate');
{
Description: 'hello function',
FunctionName: 'hello',
Handler: 'index.js',
MemorySize: 128,
Role: tfstate('data.aws_iam_role.lambda.arn'),
Runtime: 'nodejs20.x',
Timeout: 5,
TracingConfig: {
Mode: 'PassThrough',
},
VpcConfig: {
SubnetIds: [
tfstate('aws_subnet.lambda["az-a"].id'),
tfstate('aws_subnet.lambda["az-b"].id'),
],
SecurityGroupIds: [
tfstate('aws_security_group.internal["%s"].id' % must_env('WORLD')),
],
},
}
```

Likewise, if you have AWS resource definitions spread across multiple tfstate files, you can utilize `--prefixed-tfstate` option:

e.g.
Expand All @@ -530,28 +644,23 @@ which then exposes additional template functions available like:
}
```

### Jsonnet support for function configuration

lambroll also can read function.jsonnet as [Jsonnet](https://jsonnet.org/) format instead of plain JSON.
For Jsonnet, a `{prefix}_tfstate` native function is generated by the `--prefixed-tfstate` option.

```jsonnet
local first_tfstate = std.native('my_first_tfstate');
local second_tfstate = std.native('my_second_tfstate');
{
FunctionName: 'hello',
Handler: 'index.handler',
MemorySize: std.extVar('memorySize'),
Role: 'arn:aws:iam::%s:role/lambda_role' % [ std.extVar('accountID') ],
Runtime: 'nodejs20.x',
Description: 'hello function',
Environment: {
Variables: {
FIRST_VALUE: first_tfstate('data.aws_iam_role.lambda.arn'),
SECOND_VALUE: second_tfstate('data.aws_iam_role.lambda.arn'),
},
},
"rest of the parameters": "...",
}
```

```console
$ lambroll \
--function function.jsonnet \
--ext-str accountID=0123456789012 \
--ext-code memorySize="128 * 4" \
deploy
```

### .lambdaignore

lambroll will ignore files defined in `.lambdaignore` file at creating a zip archive.
Expand Down
73 changes: 73 additions & 0 deletions caller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package lambroll

import (
"context"
"text/template"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
)

type CallerIdentity struct {
data map[string]any
Resolver func(ctx context.Context) (*sts.GetCallerIdentityOutput, error)
}

func newCallerIdentity(cfg aws.Config) *CallerIdentity {
return &CallerIdentity{
Resolver: func(ctx context.Context) (*sts.GetCallerIdentityOutput, error) {
return sts.NewFromConfig(cfg).GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
},
}
}

func (c *CallerIdentity) resolve(ctx context.Context) error {
if c.data != nil {
return nil
}
res, err := c.Resolver(ctx)
if err != nil {
return err
}
c.data = map[string]any{
"Account": *res.Account,
"Arn": *res.Arn,
"UserId": *res.UserId,
}
return nil
}

func (c *CallerIdentity) Account(ctx context.Context) string {
if err := c.resolve(ctx); err != nil {
return ""
}
return c.data["Account"].(string)
}

func (c *CallerIdentity) JsonnetNativeFuncs(ctx context.Context) []*jsonnet.NativeFunction {
return []*jsonnet.NativeFunction{
{
Name: "caller_identity",
Params: []ast.Identifier{},
Func: func(params []any) (any, error) {
if err := c.resolve(ctx); err != nil {
return nil, err
}
return c.data, nil
},
},
}
}

func (c *CallerIdentity) FuncMap(ctx context.Context) template.FuncMap {
return template.FuncMap{
"caller_identity": func() map[string]any {
if err := c.resolve(ctx); err != nil {
return nil
}
return c.data
},
}
}
25 changes: 25 additions & 0 deletions caller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lambroll_test

import (
"context"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/fujiwara/lambroll"
)

func TestCallerIdentity(t *testing.T) {
c := lambroll.NewCallerIdentity(aws.Config{})
c.Resolver = func(_ context.Context) (*sts.GetCallerIdentityOutput, error) {
return &sts.GetCallerIdentityOutput{
Account: aws.String("123456789012"),
Arn: aws.String("arn:aws:iam::123456789012:user/test-user"),
UserId: aws.String("AIXXXXXXXXX"),
}, nil
}
ctx := context.Background()
if c.Account(ctx) != "123456789012" {
t.Errorf("unexpected account id: %s", c.Account(ctx))
}
}
12 changes: 12 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ var (
LoadZipArchive = loadZipArchive
MergeTags = mergeTags
FillDefaultValues = fillDefaultValues
JSONStr = jsonStr
MarshalJSON = marshalJSON
NewFunctionFrom = newFunctionFrom
NewCallerIdentity = newCallerIdentity
)

type VersionsOutput = versionsOutput
type VersionsOutputs = versionsOutputs

func (app *App) CallerIdentity() *CallerIdentity {
return app.callerIdentity
}

func (app *App) LoadFunction(f string) (*Function, error) {
return app.loadFunction(f)
}
Loading
Loading