From b336ba74ccb206a17d52bf9e1987accac40da841 Mon Sep 17 00:00:00 2001 From: Greg Tyler Date: Mon, 23 Oct 2023 14:58:12 +0100 Subject: [PATCH] Add baseline code for other API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add code for get ad update lambdas - Add OpenAPI spec - Build lambda containers in CI - Deploy lambdas for each endpoint - Update tests to This includes adding `docs/schemas/lpa.json`, which can serve as a public JSON Schema definition _and_ get used in the OpenAPI doc. For VEGA-2104 #minor --- .github/workflows/build-push-images.yml | 4 + Makefile | 8 +- docker-compose.yml | 36 +++++++- docs/openapi/openapi.yaml | 99 +++++++++++++++++++- docs/schemas/lpa.json | 14 +++ go.work | 2 + go.work.sum | 5 +- lambda/Dockerfile | 2 +- lambda/create/main.go | 4 +- lambda/get/go.mod | 11 +++ lambda/get/go.sum | 14 +++ lambda/get/main.go | 56 +++++++++++ lambda/shared/client.go | 5 +- lambda/shared/ddb.go | 26 +++++- lambda/shared/go.mod | 5 + lambda/shared/go.sum | 8 ++ lambda/shared/{case.go => lpa.go} | 2 +- lambda/shared/update.go | 12 +++ lambda/update/go.mod | 11 +++ lambda/update/go.sum | 14 +++ lambda/update/main.go | 102 +++++++++++++++++++++ mock-apigw/main.go | 17 +++- signer/main.go | 8 +- terraform/environment/region/apigateway.tf | 2 + terraform/environment/region/main.tf | 4 +- 25 files changed, 450 insertions(+), 21 deletions(-) create mode 100644 docs/schemas/lpa.json create mode 100644 lambda/get/go.mod create mode 100644 lambda/get/go.sum create mode 100644 lambda/get/main.go rename lambda/shared/{case.go => lpa.go} (91%) create mode 100644 lambda/shared/update.go create mode 100644 lambda/update/go.mod create mode 100644 lambda/update/go.sum create mode 100644 lambda/update/main.go diff --git a/.github/workflows/build-push-images.yml b/.github/workflows/build-push-images.yml index 988634b8..5a8c009a 100644 --- a/.github/workflows/build-push-images.yml +++ b/.github/workflows/build-push-images.yml @@ -37,6 +37,10 @@ jobs: include: - ecr_repository: lpa-store/lambda/api-create dir: create + - ecr_repository: lpa-store/lambda/api-get + dir: get + - ecr_repository: lpa-store/lambda/api-update + dir: update runs-on: ubuntu-latest name: ${{ matrix.ecr_repository }} steps: diff --git a/Makefile b/Makefile index 4442b908..b9df3dc9 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ export AWS_ACCESS_KEY_ID ?= X export AWS_SECRET_ACCESS_KEY ?= X build: - docker compose build --parallel lambda-create apigw + docker compose build --parallel lambda-create lambda-update lambda-get apigw up: docker compose up -d apigw @@ -16,7 +16,9 @@ test-api: URL ?= http://localhost:9000 test-api: go build -o ./signer/test-api ./signer && \ chmod +x ./signer/test-api && \ - ./signer/test-api PUT $(URL)/lpas/M-AL9A-7EY3-075D '{"uid":"M-AL9A-7EY3-075D","version":"1"}' + ./signer/test-api PUT $(URL)/lpas/M-AL9A-7EY3-075D '{"version":"1"}' && \ + ./signer/test-api POST $(URL)/lpas/M-AL9A-7EY3-075D/updates '{"type":"BUMP_VERSION","changes":[{"key":"/version","old":"1","new":"2"}]}' && \ + ./signer/test-api GET $(URL)/lpas/M-AL9A-7EY3-075D '' | grep '"version":"2"' \ create-tables: docker compose run --rm aws dynamodb describe-table --table-name deeds || \ @@ -27,7 +29,7 @@ create-tables: --billing-mode PAY_PER_REQUEST docker compose run --rm aws dynamodb describe-table --table-name events || \ - docker compose run --rm aws dynamodb create-table \ + docker compose run --rm aws dynamodb create-ta~ble \ --table-name events \ --attribute-definitions AttributeName=uid,AttributeType=S AttributeName=created,AttributeType=S \ --key-schema AttributeName=uid,KeyType=HASH AttributeName=created,KeyType=RANGE \ diff --git a/docker-compose.yml b/docker-compose.yml index 02d8fecc..01e8f989 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,42 @@ services: - "./lambda/.aws-lambda-rie:/aws-lambda" entrypoint: /aws-lambda/aws-lambda-rie /var/task/main + lambda-update: + depends_on: [ddb] + build: + context: . + dockerfile: ./lambda/Dockerfile + args: + - DIR=update + environment: + AWS_REGION: eu-west-1 + AWS_DYNAMODB_ENDPOINT: http://ddb:8000 + AWS_ACCESS_KEY_ID: X + AWS_SECRET_ACCESS_KEY: X + DDB_TABLE_NAME_DEEDS: deeds + volumes: + - "./lambda/.aws-lambda-rie:/aws-lambda" + entrypoint: /aws-lambda/aws-lambda-rie /var/task/main + + lambda-get: + depends_on: [ddb] + build: + context: . + dockerfile: ./lambda/Dockerfile + args: + - DIR=get + environment: + AWS_REGION: eu-west-1 + AWS_DYNAMODB_ENDPOINT: http://ddb:8000 + AWS_ACCESS_KEY_ID: X + AWS_SECRET_ACCESS_KEY: X + DDB_TABLE_NAME_DEEDS: deeds + volumes: + - "./lambda/.aws-lambda-rie:/aws-lambda" + entrypoint: /aws-lambda/aws-lambda-rie /var/task/main + apigw: - depends_on: [lambda-create] + depends_on: [lambda-create, lambda-update, lambda-get] build: ./mock-apigw ports: - 9000:8080 diff --git a/docs/openapi/openapi.yaml b/docs/openapi/openapi.yaml index c1a63701..60d2bbcd 100644 --- a/docs/openapi/openapi.yaml +++ b/docs/openapi/openapi.yaml @@ -32,8 +32,7 @@ paths: content: application/json: schema: - type: object - additionalProperties: false + $ref: "#/components/schemas/Lpa" responses: "201": description: Case created @@ -50,6 +49,74 @@ paths: httpMethod: "POST" type: "aws_proxy" contentHandling: "CONVERT_TO_TEXT" + get: + operationId: getLpa + summary: Retrieve an LPA + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: false + responses: + "200": + description: Case found + content: + application/json: + schema: + $ref: "#/components/schemas/Lpa" + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestError" + x-amazon-apigateway-auth: + type: "AWS_IAM" + x-amazon-apigateway-integration: + uri: ${lambda_get_invoke_arn} + httpMethod: "POST" + type: "aws_proxy" + contentHandling: "CONVERT_TO_TEXT" + /lpas/{uid}/updates: + parameters: + - name: uid + in: path + required: true + description: The UID of the complaint + schema: + type: string + pattern: "M-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})" + example: M-789Q-P4DF-4UX3 + post: + operationId: createUpdate + summary: Update an LPA + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Update" + responses: + "201": + description: Update created + content: + application/json: + schema: + type: object + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequestError" + x-amazon-apigateway-auth: + type: "AWS_IAM" + x-amazon-apigateway-integration: + uri: ${lambda_update_invoke_arn} + httpMethod: "POST" + type: "aws_proxy" + contentHandling: "CONVERT_TO_TEXT" + /health: get: operationId: healthCheck @@ -125,3 +192,31 @@ components: example: - source: "/uid" detail: "invalid uid format" + Lpa: + $ref: "../schemas/lpa.json" + Update: + type: object + required: + - type + - changes + properties: + type: + enum: + - DONOR_ADDRESS_UPDATE + - ATTORNEY_ADDRESS_UPDATE + - SCANNING_CORRECTION + changes: + type: array + items: + type: object + required: + - key + - old + - new + properties: + key: + type: string + old: + nullable: true + new: + nullable: true diff --git a/docs/schemas/lpa.json b/docs/schemas/lpa.json new file mode 100644 index 00000000..c1d946b4 --- /dev/null +++ b/docs/schemas/lpa.json @@ -0,0 +1,14 @@ +{ + "$id": "https://opg.service.justice.gov.uk/schema/lpa/2023-10", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "LPA", + "description": "Digital representation of a Lasting Power of Attorney", + "type": "object", + "properties": { + "uid": { + "type": "string", + "pattern": "M-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})", + "example": "M-789Q-P4DF-4UX3" + } + } +} diff --git a/go.work b/go.work index 227064f5..76b06e19 100644 --- a/go.work +++ b/go.work @@ -2,7 +2,9 @@ go 1.21.0 use ( ./lambda/create + ./lambda/get ./lambda/shared + ./lambda/update ./mock-apigw ./signer ) diff --git a/go.work.sum b/go.work.sum index f99474c1..2cfced49 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,8 +1,10 @@ github.com/aws/aws-sdk-go-v2 v1.6.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w= github.com/aws/aws-sdk-go-v2/service/route53 v1.6.2/go.mod h1:ZnAMilx42P7DgIrdjlWCkNIGSBLzeyk6T31uB8oGTwY= github.com/aws/smithy-go v1.4.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= @@ -15,3 +17,4 @@ golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/lambda/Dockerfile b/lambda/Dockerfile index 3014d105..27124223 100644 --- a/lambda/Dockerfile +++ b/lambda/Dockerfile @@ -1,6 +1,5 @@ FROM golang:1.21.0 AS build-env -ARG DIR WORKDIR /app COPY go.work . @@ -8,6 +7,7 @@ COPY go.work.sum . COPY . . +ARG DIR RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o /go/bin/main ./lambda/$DIR FROM alpine:3 diff --git a/lambda/create/main.go b/lambda/create/main.go index aa14207f..1f28e9ce 100644 --- a/lambda/create/main.go +++ b/lambda/create/main.go @@ -25,7 +25,7 @@ type Lambda struct { } func (l *Lambda) HandleEvent(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - var data shared.Case + var data shared.Lpa response := events.APIGatewayProxyResponse{ StatusCode: 500, Body: "{\"code\":\"INTERNAL_SERVER_ERROR\",\"detail\":\"Internal server error\"}", @@ -37,6 +37,8 @@ func (l *Lambda) HandleEvent(ctx context.Context, event events.APIGatewayProxyRe return shared.ProblemInternalServerError.Respond() } + data.Uid = event.PathParameters["uid"] + if data.Version == "" { problem := shared.ProblemInvalidRequest problem.Errors = []shared.FieldError{ diff --git a/lambda/get/go.mod b/lambda/get/go.mod new file mode 100644 index 00000000..68158245 --- /dev/null +++ b/lambda/get/go.mod @@ -0,0 +1,11 @@ +module github.com/ministryofjustice/opg-data-lpa-deed/lambda/get + +go 1.21.0 + +toolchain go1.21.3 + +require ( + github.com/aws/aws-lambda-go v1.41.0 + github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db + github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90 +) diff --git a/lambda/get/go.sum b/lambda/get/go.sum new file mode 100644 index 00000000..bfef836f --- /dev/null +++ b/lambda/get/go.sum @@ -0,0 +1,14 @@ +github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= +github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db h1:HcdoeSkWe5Bkokl3SvmaOlPNsCk+T78oQqVDrFNgsD8= +github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db/go.mod h1:uarvaw7JMaubij8CuiO2bNcJBp8zWEdiU+AVqe78Ggc= +github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90 h1:mxTHIeCYV7LDZPN7C44wwLlBTUsgQ0G8FQprsrsKXaA= +github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90/go.mod h1:1RmCNi6dkAv8umAgNHp8RkuBoSKLlxp1UtfsGYH7ufc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lambda/get/main.go b/lambda/get/main.go new file mode 100644 index 00000000..e68832a2 --- /dev/null +++ b/lambda/get/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "encoding/json" + "os" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared" + "github.com/ministryofjustice/opg-go-common/logging" +) + +type Logger interface { + Print(...interface{}) +} + +type Lambda struct { + store shared.Client + logger Logger +} + +func (l *Lambda) HandleEvent(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + response := events.APIGatewayProxyResponse{ + StatusCode: 500, + Body: "{\"code\":\"INTERNAL_SERVER_ERROR\",\"detail\":\"Internal server error\"}", + } + + lpa, err := l.store.Get(ctx, event.PathParameters["uid"]) + + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + body, err := json.Marshal(lpa) + + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + response.StatusCode = 200 + response.Body = string(body) + + return response, nil +} + +func main() { + l := &Lambda{ + store: shared.NewDynamoDB(os.Getenv("DDB_TABLE_NAME_DEEDS")), + logger: logging.New(os.Stdout, "opg-data-lpa-deed"), + } + + lambda.Start(l.HandleEvent) +} diff --git a/lambda/shared/client.go b/lambda/shared/client.go index 1885aa3f..abff0de6 100644 --- a/lambda/shared/client.go +++ b/lambda/shared/client.go @@ -5,7 +5,6 @@ import ( ) type Client interface { - Put(ctx context.Context, data Case) error - // Get(ctx context.Context, id string) (Case, error) - // Patch(ctx context.Context, id string, data Update) (Case, error) + Put(ctx context.Context, data Lpa) error + Get(ctx context.Context, uid string) (Lpa, error) } diff --git a/lambda/shared/ddb.go b/lambda/shared/ddb.go index cba72bf0..908beb39 100644 --- a/lambda/shared/ddb.go +++ b/lambda/shared/ddb.go @@ -16,7 +16,7 @@ type DynamoDBClient struct { tableName string } -func (c DynamoDBClient) Put(ctx context.Context, data Case) error { +func (c DynamoDBClient) Put(ctx context.Context, data Lpa) error { item, err := dynamodbattribute.MarshalMap(data) if err != nil { return err @@ -30,6 +30,30 @@ func (c DynamoDBClient) Put(ctx context.Context, data Case) error { return err } +func (c DynamoDBClient) Get(ctx context.Context, uid string) (Lpa, error) { + lpa := Lpa{} + + marshalledUid, err := dynamodbattribute.Marshal(uid) + if err != nil { + return lpa, err + } + + getItemOutput, err := c.ddb.GetItemWithContext(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(c.tableName), + Key: map[string]*dynamodb.AttributeValue{ + "uid": marshalledUid, + }, + }) + + if err != nil { + return lpa, err + } + + err = dynamodbattribute.UnmarshalMap(getItemOutput.Item, &lpa) + + return lpa, err +} + func NewDynamoDB(tableName string) DynamoDBClient { sess := session.Must(session.NewSession()) diff --git a/lambda/shared/go.mod b/lambda/shared/go.mod index 0e0af537..b6d8b81b 100644 --- a/lambda/shared/go.mod +++ b/lambda/shared/go.mod @@ -10,9 +10,13 @@ require ( require ( github.com/andybalholm/brotli v1.0.4 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.15.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect @@ -22,4 +26,5 @@ require ( google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f // indirect google.golang.org/grpc v1.35.0 // indirect google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lambda/shared/go.sum b/lambda/shared/go.sum index 80649460..d8bc3605 100644 --- a/lambda/shared/go.sum +++ b/lambda/shared/go.sum @@ -20,6 +20,10 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -46,8 +50,12 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/lambda/shared/case.go b/lambda/shared/lpa.go similarity index 91% rename from lambda/shared/case.go rename to lambda/shared/lpa.go index a4783826..1bfc3852 100644 --- a/lambda/shared/case.go +++ b/lambda/shared/lpa.go @@ -2,7 +2,7 @@ package shared import "time" -type Case struct { +type Lpa struct { Uid string `json:"uid" dynamodbav:"uid"` Version string `json:"version" dynamodbav:"version"` UpdatedAt time.Time `json:"-" dynamodbav:"updatedAt"` diff --git a/lambda/shared/update.go b/lambda/shared/update.go new file mode 100644 index 00000000..b1e4727f --- /dev/null +++ b/lambda/shared/update.go @@ -0,0 +1,12 @@ +package shared + +type Change struct { + Key string `json:"key"` + Old interface{} `json:"old"` + New interface{} `json:"new"` +} + +type Update struct { + Type string `json:"type"` + Changes []Change `json:"changes"` +} diff --git a/lambda/update/go.mod b/lambda/update/go.mod new file mode 100644 index 00000000..28c68e56 --- /dev/null +++ b/lambda/update/go.mod @@ -0,0 +1,11 @@ +module github.com/ministryofjustice/opg-data-lpa-deed/lambda/update + +go 1.21.0 + +toolchain go1.21.3 + +require ( + github.com/aws/aws-lambda-go v1.41.0 + github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db + github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90 +) diff --git a/lambda/update/go.sum b/lambda/update/go.sum new file mode 100644 index 00000000..bfef836f --- /dev/null +++ b/lambda/update/go.sum @@ -0,0 +1,14 @@ +github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= +github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db h1:HcdoeSkWe5Bkokl3SvmaOlPNsCk+T78oQqVDrFNgsD8= +github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared v0.0.0-20231012101804-da267f23d7db/go.mod h1:uarvaw7JMaubij8CuiO2bNcJBp8zWEdiU+AVqe78Ggc= +github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90 h1:mxTHIeCYV7LDZPN7C44wwLlBTUsgQ0G8FQprsrsKXaA= +github.com/ministryofjustice/opg-go-common v0.0.0-20220816144329-763497f29f90/go.mod h1:1RmCNi6dkAv8umAgNHp8RkuBoSKLlxp1UtfsGYH7ufc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lambda/update/main.go b/lambda/update/main.go new file mode 100644 index 00000000..ef3b9ca2 --- /dev/null +++ b/lambda/update/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/go-openapi/jsonpointer" + "github.com/ministryofjustice/opg-data-lpa-deed/lambda/shared" + "github.com/ministryofjustice/opg-go-common/logging" +) + +type Logger interface { + Print(...interface{}) +} + +type Lambda struct { + store shared.Client + logger Logger +} + +func (l *Lambda) HandleEvent(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + var update shared.Update + response := events.APIGatewayProxyResponse{ + StatusCode: 500, + Body: "{\"code\":\"INTERNAL_SERVER_ERROR\",\"detail\":\"Internal server error\"}", + } + + err := json.Unmarshal([]byte(event.Body), &update) + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + lpa, err := l.store.Get(ctx, event.PathParameters["uid"]) + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + err = applyUpdate(&lpa, update) + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + err = l.store.Put(ctx, lpa) + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + body, err := json.Marshal(lpa) + + if err != nil { + l.logger.Print(err) + return shared.ProblemInternalServerError.Respond() + } + + response.StatusCode = 201 + response.Body = string(body) + + return response, nil +} + +func applyUpdate(lpa *shared.Lpa, update shared.Update) error { + for _, change := range update.Changes { + pointer, err := jsonpointer.New(change.Key) + if err != nil { + return err + } + + current, _, err := pointer.Get(*lpa) + if err != nil { + return err + } + + if current != change.Old { + err = fmt.Errorf("existing value for %s does not match request", change.Key) + return err + } + + _, err = pointer.Set(lpa, change.New) + if err != nil { + return err + } + } + + return nil +} + +func main() { + l := &Lambda{ + store: shared.NewDynamoDB(os.Getenv("DDB_TABLE_NAME_DEEDS")), + logger: logging.New(os.Stdout, "opg-data-lpa-deed"), + } + + lambda.Start(l.HandleEvent) +} diff --git a/mock-apigw/main.go b/mock-apigw/main.go index 55414896..8a2f4e66 100644 --- a/mock-apigw/main.go +++ b/mock-apigw/main.go @@ -13,13 +13,22 @@ import ( "github.com/aws/aws-lambda-go/events" ) -var LPAPath = regexp.MustCompile("/lpas/M(-[0-9A-Z]{4}){3}") +var LPAPath = regexp.MustCompile("^/lpas/(M(?:-[0-9A-Z]{4}){3})$") +var UpdatePath = regexp.MustCompile("^/lpas/(M(?:-[0-9A-Z]{4}){3})/updates$") func delegateHandler(w http.ResponseWriter, r *http.Request) { lambdaName := "" + uid := "" if LPAPath.MatchString(r.URL.Path) && r.Method == http.MethodPut { + uid = LPAPath.FindStringSubmatch(r.URL.Path)[1] lambdaName = "create" + } else if LPAPath.MatchString(r.URL.Path) && r.Method == http.MethodGet { + uid = LPAPath.FindStringSubmatch(r.URL.Path)[1] + lambdaName = "get" + } else if UpdatePath.MatchString(r.URL.Path) && r.Method == http.MethodPost { + uid = UpdatePath.FindStringSubmatch(r.URL.Path)[1] + lambdaName = "update" } if lambdaName == "" { @@ -33,7 +42,11 @@ func delegateHandler(w http.ResponseWriter, r *http.Request) { _, _ = io.Copy(reqBody, r.Body) body := events.APIGatewayProxyRequest{ - Body: reqBody.String(), + Body: reqBody.String(), + Path: r.URL.Path, + PathParameters: map[string]string{ + "uid": uid, + }, HTTPMethod: r.Method, MultiValueHeaders: r.Header, } diff --git a/signer/main.go b/signer/main.go index 1ba4f050..1b168eda 100644 --- a/signer/main.go +++ b/signer/main.go @@ -36,12 +36,14 @@ func main() { panic(err) } + buf := new(strings.Builder) + _, _ = io.Copy(buf, resp.Body) + if resp.StatusCode >= 400 { log.Printf("Response code %d", resp.StatusCode) - buf := new(strings.Builder) - _, _ = io.Copy(buf, resp.Body) log.Printf("error response: %s", buf.String()) - panic(fmt.Sprintf("invalid status code %d", resp.StatusCode)) } + + os.Stdout.WriteString(fmt.Sprintf("%d: %s\n", resp.StatusCode, buf.String())) } diff --git a/terraform/environment/region/apigateway.tf b/terraform/environment/region/apigateway.tf index 2211b5af..f8077c65 100644 --- a/terraform/environment/region/apigateway.tf +++ b/terraform/environment/region/apigateway.tf @@ -2,6 +2,8 @@ locals { stage_name = "current" template_file = templatefile("../../docs/openapi/openapi.yaml", { lambda_create_invoke_arn = module.lambda["create"].invoke_arn + lambda_get_invoke_arn = module.lambda["get"].invoke_arn + lambda_update_invoke_arn = module.lambda["update"].invoke_arn }) } diff --git a/terraform/environment/region/main.tf b/terraform/environment/region/main.tf index 9b595f64..c89196a0 100644 --- a/terraform/environment/region/main.tf +++ b/terraform/environment/region/main.tf @@ -1,8 +1,8 @@ locals { functions = toset([ "create", - # "get", - # "update", + "get", + "update", ]) }