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/.github/workflows/env-test.yml b/.github/workflows/env-test.yml index c67686e4..6a0a384a 100644 --- a/.github/workflows/env-test.yml +++ b/.github/workflows/env-test.yml @@ -46,7 +46,7 @@ jobs: role-to-assume: arn:aws:iam::493907465011:role/lpa-store-ci role-duration-seconds: 3600 role-session-name: GitHubActions - - name: POST to server + - name: Run test suite env: URL: ${{ inputs.base_url }} run: make test-api diff --git a/.redocly.lint-ignore.yaml b/.redocly.lint-ignore.yaml new file mode 100644 index 00000000..97bf2dae --- /dev/null +++ b/.redocly.lint-ignore.yaml @@ -0,0 +1,12 @@ +# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API. +# See https://redoc.ly/docs/cli/ for more information. +docs/schemas/lpa.json: + spec: + - '#/$id' + - '#/$schema' +docs/openapi/openapi.yaml: + spec: + - >- + #/components/schemas/Update/properties/changes/items/properties/old/nullable + - >- + #/components/schemas/Update/properties/changes/items/properties/new/nullable diff --git a/Makefile b/Makefile index 4442b908..a15715af 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 || \ 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..56f7346d 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 }) } @@ -129,10 +131,11 @@ data "aws_iam_policy_document" "lpa_store" { } } -resource "aws_lambda_permission" "api_gateway_can_create" { +resource "aws_lambda_permission" "api_gateway_invoke" { + for_each = module.lambda statement_id = "AllowLambdaAPIGatewayInvocation" action = "lambda:InvokeFunction" - function_name = module.lambda["create"].function_name + function_name = each.value.function_name principal = "apigateway.amazonaws.com" # The /* part allows invocation from any stage, method and resource path # within API Gateway. 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", ]) }