Skip to content

Commit

Permalink
Merge pull request #429 from fujiwara/jsonnet-native-funcs
Browse files Browse the repository at this point in the history
Add Jsonnet native functions.
  • Loading branch information
fujiwara authored Aug 29, 2024
2 parents 8161675 + 8ca36e6 commit 0a5374a
Show file tree
Hide file tree
Showing 12 changed files with 616 additions and 179 deletions.
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

0 comments on commit 0a5374a

Please sign in to comment.