diff --git a/events/README_SecretsManager_SecretRotationEvent.md b/events/README_SecretsManager_SecretRotationEvent.md new file mode 100644 index 00000000..1b572cf6 --- /dev/null +++ b/events/README_SecretsManager_SecretRotationEvent.md @@ -0,0 +1,38 @@ +# Sample Function + +The following is a sample Lambda function that handles a SecretsManager secret rotation event. + +```go +package main + +import ( + "fmt" + "context" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/events" +) + +func handler(ctx context.Context, event SecretsManagerSecretRotationEvent) error { + fmt.Printf("rotating secret %s with token %s\n", + event.SecretID, event.ClientRequestToken) + + switch event.Step { + case "createSecret": + // create + case "setSecret": + // set + case "finishSecret": + // finish + case "testSecret": + // test + } + + return nil +} + + +func main() { + lambda.Start(handler) +} +``` \ No newline at end of file diff --git a/events/code_commit.go b/events/code_commit.go index 8bb6d880..9d5e071c 100644 --- a/events/code_commit.go +++ b/events/code_commit.go @@ -91,11 +91,12 @@ type CodeCommitReference struct { Commit string `json:"commit"` Ref string `json:"ref"` Created bool `json:"created,omitempty"` + Deleted bool `json:"deleted,omitempty"` } // String returns a string representation of this object. // Useful for testing and debugging. func (r CodeCommitReference) String() string { return fmt.Sprintf( - "{commit: %v, ref: %v, created: %v}", r.Commit, r.Ref, r.Created) + "{commit: %v, ref: %v, created: %v, deleted: %v}", r.Commit, r.Ref, r.Created, r.Deleted) } diff --git a/events/code_commit_test.go b/events/code_commit_test.go index 493971df..32c163a3 100644 --- a/events/code_commit_test.go +++ b/events/code_commit_test.go @@ -28,6 +28,16 @@ func TestCodeCommitReference(t *testing.T) { "ref": "refs/heads/master", "created": true } + `), + }, + { + Name: "Deleted CodeCommitReference", + Input: []byte(` + { + "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b", + "ref": "refs/heads/master", + "deleted": true + } `), }, } @@ -62,6 +72,11 @@ func TestCodeCommitCodeCommit(t *testing.T) { "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b", "ref": "refs/heads/master", "created": true + }, + { + "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b", + "ref": "refs/heads/master", + "deleted": true } ] } diff --git a/events/secretsmanager.go b/events/secretsmanager.go new file mode 100644 index 00000000..fd2bea10 --- /dev/null +++ b/events/secretsmanager.go @@ -0,0 +1,11 @@ +package events + +// SecretsManagerSecretRotationEvent is the event passed to a Lambda function to handle +// automatic secret rotation. +// +// https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets.html#rotate-secrets_how +type SecretsManagerSecretRotationEvent struct { + Step string `json:"Step"` + SecretID string `json:"SecretId"` + ClientRequestToken string `json:"ClientRequestToken"` +} diff --git a/events/secretsmanager_test.go b/events/secretsmanager_test.go new file mode 100644 index 00000000..0333bb83 --- /dev/null +++ b/events/secretsmanager_test.go @@ -0,0 +1,30 @@ +package events + +import ( + "encoding/json" + "testing" + + "github.com/aws/aws-lambda-go/events/test" + "github.com/stretchr/testify/assert" +) + +func TestSecretsManagerSecretRotationEventMarshaling(t *testing.T) { + + // 1. read JSON from file + inputJSON := test.ReadJSONFromFile(t, "./testdata/secretsmanager-secret-rotation-event.json") + + // 2. de-serialize into Go object + var inputEvent SecretsManagerSecretRotationEvent + if err := json.Unmarshal(inputJSON, &inputEvent); err != nil { + t.Errorf("could not unmarshal event. details: %v", err) + } + + // 3. serialize to JSON + outputJSON, err := json.Marshal(inputEvent) + if err != nil { + t.Errorf("could not marshal event. details: %v", err) + } + + // 4. check result + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} diff --git a/events/testdata/secretsmanager-secret-rotation-event.json b/events/testdata/secretsmanager-secret-rotation-event.json new file mode 100644 index 00000000..5f444cd0 --- /dev/null +++ b/events/testdata/secretsmanager-secret-rotation-event.json @@ -0,0 +1,5 @@ +{ + "Step": "createSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:111122223333:secret:id-ABCD1E", + "ClientRequestToken": "1ab23456-cde7-8912-34fg-h56i78j9k12l" +} \ No newline at end of file diff --git a/lambda/handler.go b/lambda/handler.go index f43fadc6..e05c6096 100644 --- a/lambda/handler.go +++ b/lambda/handler.go @@ -22,13 +22,15 @@ type Handler interface { type handlerOptions struct { handlerFunc - baseContext context.Context - contextValues map[interface{}]interface{} - jsonResponseEscapeHTML bool - jsonResponseIndentPrefix string - jsonResponseIndentValue string - enableSIGTERM bool - sigtermCallbacks []func() + baseContext context.Context + contextValues map[interface{}]interface{} + jsonRequestUseNumber bool + jsonRequestDisallowUnknownFields bool + jsonResponseEscapeHTML bool + jsonResponseIndentPrefix string + jsonResponseIndentValue string + enableSIGTERM bool + sigtermCallbacks []func() } type Option func(*handlerOptions) @@ -99,6 +101,38 @@ func WithSetIndent(prefix, indent string) Option { }) } +// WithUseNumber sets the UseNumber option on the underlying json decoder +// +// Usage: +// +// lambda.StartWithOptions( +// func (event any) (any, error) { +// return event, nil +// }, +// lambda.WithUseNumber(true) +// ) +func WithUseNumber(useNumber bool) Option { + return Option(func(h *handlerOptions) { + h.jsonRequestUseNumber = useNumber + }) +} + +// WithUseNumber sets the DisallowUnknownFields option on the underlying json decoder +// +// Usage: +// +// lambda.StartWithOptions( +// func (event any) (any, error) { +// return event, nil +// }, +// lambda.WithDisallowUnknownFields(true) +// ) +func WithDisallowUnknownFields(disallowUnknownFields bool) Option { + return Option(func(h *handlerOptions) { + h.jsonRequestDisallowUnknownFields = disallowUnknownFields + }) +} + // WithEnableSIGTERM enables SIGTERM behavior within the Lambda platform on container spindown. // SIGKILL will occur ~500ms after SIGTERM. // Optionally, an array of callback functions to run on SIGTERM may be provided. @@ -289,6 +323,12 @@ func reflectHandler(f interface{}, h *handlerOptions) handlerFunc { out.Reset() in := bytes.NewBuffer(payload) decoder := json.NewDecoder(in) + if h.jsonRequestUseNumber { + decoder.UseNumber() + } + if h.jsonRequestDisallowUnknownFields { + decoder.DisallowUnknownFields() + } encoder := json.NewEncoder(out) encoder.SetEscapeHTML(h.jsonResponseEscapeHTML) encoder.SetIndent(h.jsonResponseIndentPrefix, h.jsonResponseIndentValue) diff --git a/lambda/handler_test.go b/lambda/handler_test.go index 87942900..6eba8798 100644 --- a/lambda/handler_test.go +++ b/lambda/handler_test.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/ioutil" //nolint: staticcheck + "reflect" "strings" "testing" "time" @@ -309,6 +310,54 @@ func TestInvokes(t *testing.T) { }, options: []Option{WithSetIndent(">>", " ")}, }, + { + name: "WithUseNumber(true) results in json.Number instead of float64 when decoding to an interface{}", + input: `19.99`, + expected: expected{`"Number"`, nil}, + handler: func(event interface{}) (string, error) { + return reflect.TypeOf(event).Name(), nil + }, + options: []Option{WithUseNumber(true)}, + }, + { + name: "WithUseNumber(false)", + input: `19.99`, + expected: expected{`"float64"`, nil}, + handler: func(event interface{}) (string, error) { + return reflect.TypeOf(event).Name(), nil + }, + options: []Option{WithUseNumber(false)}, + }, + { + name: "No decoder options provided is the same as WithUseNumber(false)", + input: `19.99`, + expected: expected{`"float64"`, nil}, + handler: func(event interface{}) (string, error) { + return reflect.TypeOf(event).Name(), nil + }, + options: []Option{}, + }, + { + name: "WithDisallowUnknownFields(true)", + input: `{"Hello": "World"}`, + expected: expected{"", errors.New(`json: unknown field "Hello"`)}, + handler: func(_ struct{}) {}, + options: []Option{WithDisallowUnknownFields(true)}, + }, + { + name: "WithDisallowUnknownFields(false)", + input: `{"Hello": "World"}`, + expected: expected{`null`, nil}, + handler: func(_ struct{}) {}, + options: []Option{WithDisallowUnknownFields(false)}, + }, + { + name: "No decoder options provided is the same as WithDisallowUnknownFields(false)", + input: `{"Hello": "World"}`, + expected: expected{`null`, nil}, + handler: func(_ struct{}) {}, + options: []Option{}, + }, { name: "bytes are base64 encoded strings", input: `"aGVsbG8="`,