diff --git a/.codecov.yml b/.codecov.yml index a44b194ab8..ed122151c1 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -11,6 +11,7 @@ coverage: - "./cmd/mock-notify/main.go" - "./cmd/mock-onelogin/main.go" - "./cmd/mock-os-api/main.go" + - "./cmd/schedule-runner/main.go" - "./internal/identity/yoti*" - "./internal/notify/email.go" - "./internal/notify/sms.go" @@ -18,6 +19,7 @@ coverage: - "./internal/telemetry" - "./internal/validation/error.go" - "./mocks/*" + - "./scripts/*.go" status: project: default: diff --git a/.github/workflows/docker_job.yml b/.github/workflows/docker_job.yml index 55b03afd60..d479ba7d42 100644 --- a/.github/workflows/docker_job.yml +++ b/.github/workflows/docker_job.yml @@ -53,6 +53,11 @@ jobs: path: ./docker/mock-pay/Dockerfile trivyignores: ./docker/mock-pay/.trivyignore.yaml platforms: linux/amd64 + - ecr_repository: modernising-lpa/schedule-runner + name: schedule-runner + path: ./docker/schedule-runner/Dockerfile + trivyignores: ./docker/schedule-runner/.trivyignore.yaml + platforms: linux/amd64 runs-on: ubuntu-latest name: ${{ matrix.ecr_repository }} diff --git a/Makefile b/Makefile index 75a736185a..f0f48ed61e 100644 --- a/Makefile +++ b/Makefile @@ -158,8 +158,8 @@ set-uploads-infected: ##@events calls emit-object-tags-added-with-virus for all key=$$k $(MAKE) emit-object-tags-added-with-virus ; \ done -tail-logs: ##@app tails logs for app mock-notify, events-lambda, mock-onelogin, mock-lpa-store and mock-uid and filters out noisy runner logs - docker compose --ansi=always -f docker/docker-compose.yml -f docker/docker-compose.dev.yml logs app mock-notify events-lambda mock-onelogin mock-lpa-store mock-uid -f | grep -v 'runner' +tail-logs: ##@app tails logs for app mock-notify, events-lambda, schedule-runner-lambda, mock-onelogin, mock-lpa-store and mock-uid + docker compose --ansi=always -f docker/docker-compose.yml -f docker/docker-compose.dev.yml logs app mock-notify events-lambda schedule-runner-lambda mock-onelogin mock-lpa-store mock-uid -f terraform-update-docs: ##@terraform updates all terraform-docs managed documentation terraform-docs --config terraform/environment/.terraform-docs.yml ./terraform/environment @@ -173,3 +173,6 @@ delete-all-from-lpa-index: ##@opensearch clears all items from the lpa index delete-lpa-index: ##@opensearch deletes the lpa index curl -XDELETE "http://localhost:9200/lpas" + +add-scheduled-events: + go run ./scripts/add-scheduled-items.go diff --git a/cmd/mlpa/main.go b/cmd/mlpa/main.go index 0597121f18..733c66a944 100644 --- a/cmd/mlpa/main.go +++ b/cmd/mlpa/main.go @@ -23,7 +23,6 @@ import ( "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/app" - "github.com/ministryofjustice/opg-modernising-lpa/internal/donor" "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" "github.com/ministryofjustice/opg-modernising-lpa/internal/event" "github.com/ministryofjustice/opg-modernising-lpa/internal/lambda" @@ -35,7 +34,6 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/pay" "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/s3" - "github.com/ministryofjustice/opg-modernising-lpa/internal/scheduled" "github.com/ministryofjustice/opg-modernising-lpa/internal/search" "github.com/ministryofjustice/opg-modernising-lpa/internal/secrets" "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" @@ -116,14 +114,8 @@ func run(ctx context.Context, logger *slog.Logger) error { searchEndpoint = os.Getenv("SEARCH_ENDPOINT") searchIndexName = cmp.Or(os.Getenv("SEARCH_INDEX_NAME"), "lpas") searchIndexingEnabled = os.Getenv("SEARCH_INDEXING_DISABLED") != "1" - scheduledRunnerPeriod = cmp.Or(os.Getenv("SCHEDULED_RUNNER_PERIOD"), "6h") ) - scheduledRunnerPeriodDur, err := time.ParseDuration(scheduledRunnerPeriod) - if err != nil { - return err - } - staticHash, err := dirhash.HashDir(webDir+"/static", webDir, dirhash.DefaultHash) if err != nil { return err @@ -343,17 +335,6 @@ func run(ctx context.Context, logger *slog.Logger) error { handler = telemetry.WrapHandler(mux) } - donorStore := donor.NewStore(lpasDynamoClient, eventClient, logger, searchClient) - scheduledStore := scheduled.NewStore(lpasDynamoClient) - - runner := scheduled.NewRunner(logger, scheduledStore, donorStore, notifyClient, scheduledRunnerPeriodDur) - go func() { - if err := runner.Run(ctx); err != nil { - logger.Error("runner error", slog.Any("err", err)) - os.Exit(1) - } - }() - server := &http.Server{ Addr: ":" + port, Handler: page.Recover(tmpls.Get("error-500.gohtml"), logger, bundle, handler), diff --git a/cmd/schedule-runner/main.go b/cmd/schedule-runner/main.go new file mode 100644 index 0000000000..40a78109f9 --- /dev/null +++ b/cmd/schedule-runner/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "cmp" + "context" + "fmt" + "log/slog" + "net/http" + "os" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/ministryofjustice/opg-modernising-lpa/internal/donor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/event" + "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/scheduled" + "github.com/ministryofjustice/opg-modernising-lpa/internal/search" + "github.com/ministryofjustice/opg-modernising-lpa/internal/secrets" +) + +func handleRunSchedule(ctx context.Context) error { + var ( + eventBusName = cmp.Or(os.Getenv("EVENT_BUS_NAME"), "default") + notifyBaseURL = os.Getenv("GOVUK_NOTIFY_BASE_URL") + notifyIsProduction = os.Getenv("GOVUK_NOTIFY_IS_PRODUCTION") == "1" + searchEndpoint = os.Getenv("SEARCH_ENDPOINT") + searchIndexName = cmp.Or(os.Getenv("SEARCH_INDEX_NAME"), "lpas") + searchIndexingEnabled = os.Getenv("SEARCH_INDEXING_DISABLED") != "1" + tableName = os.Getenv("LPAS_TABLE") + ) + + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil). + WithAttrs([]slog.Attr{ + slog.String("service_name", "opg-modernising-lpa/schedule-runner"), + })) + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return fmt.Errorf("failed to load default config: %w", err) + } + + secretsClient, err := secrets.NewClient(cfg, time.Hour) + if err != nil { + return err + } + + notifyApiKey, err := secretsClient.Secret(ctx, secrets.GovUkNotify) + if err != nil { + return fmt.Errorf("failed to get notify API secret: %w", err) + } + + bundle, err := localize.NewBundle("./lang/en.json", "./lang/cy.json") + if err != nil { + return err + } + + notifyClient, err := notify.New(logger, notifyIsProduction, notifyBaseURL, notifyApiKey, http.DefaultClient, event.NewClient(cfg, eventBusName), bundle) + if err != nil { + return err + } + + dynamoClient, err := dynamo.NewClient(cfg, tableName) + if err != nil { + return fmt.Errorf("failed to create dynamodb client: %w", err) + } + + eventClient := event.NewClient(cfg, eventBusName) + + searchClient, err := search.NewClient(cfg, searchEndpoint, searchIndexName, searchIndexingEnabled) + if err != nil { + return err + } + + donorStore := donor.NewStore(dynamoClient, eventClient, logger, searchClient) + scheduledStore := scheduled.NewStore(dynamoClient) + + runner := scheduled.NewRunner(logger, scheduledStore, donorStore, notifyClient) + + if err := runner.Run(ctx); err != nil { + logger.Error("runner error", slog.Any("err", err)) + return err + } + + return nil +} + +func main() { + lambda.Start(handleRunSchedule) +} diff --git a/docker/event-received/aws-lambda-rie/aws-lambda-rie b/docker/aws-lambda-rie/aws-lambda-rie similarity index 100% rename from docker/event-received/aws-lambda-rie/aws-lambda-rie rename to docker/aws-lambda-rie/aws-lambda-rie diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 22ffe55b4f..b41728cfec 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -10,6 +10,7 @@ services: container_name: app-dev depends_on: - events-lambda + - schedule-runner-lambda security_opt: - "seccomp:unconfined" volumes: @@ -49,6 +50,28 @@ services: - "9000:8080" entrypoint: aws-lambda-rie /var/task/event-received + schedule-runner-lambda: + build: + context: .. + dockerfile: docker/schedule-runner/Dockerfile + target: dev + platforms: + - "linux/amd64" + - "linux/arm64" + container_name: schedule-runner + environment: + - AWS_ACCESS_KEY_ID=fakeKeyId + - AWS_BASE_URL=http://localstack:4566 + - AWS_REGION=eu-west-1 + - AWS_SECRET_ACCESS_KEY=fakeAccessKey + - LPAS_TABLE=lpas + - GOVUK_NOTIFY_IS_PRODUCTION=0 + - GOVUK_NOTIFY_BASE_URL=http://mock-notify:8080 + - SEARCH_ENDPOINT=http://my-domain.eu-west-1.opensearch.localhost.localstack.cloud:4566 + ports: + - "9002:8080" + entrypoint: aws-lambda-rie /var/task/schedule-runner + mock-pay: image: outofcoffee/imposter:latest container_name: mock-pay diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 71caf63f8e..8c868d2e3b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -60,7 +60,7 @@ services: - "/var/run/docker.sock:/var/run/docker.sock" environment: - DOCKER_HOST=unix:///var/run/docker.sock - - SERVICES=s3,secretsmanager,sqs,dynamodb,events,kms,lambda,opensearch + - SERVICES=s3,secretsmanager,sqs,dynamodb,events,kms,lambda,opensearch,scheduler,logs - DATA_DIR=/tmp/localstack/data - DEBUG=1 - AWS_ACCESS_KEY_ID=fakeKeyId diff --git a/docker/event-received/Dockerfile b/docker/event-received/Dockerfile index cc211ede3c..19865b7ef6 100644 --- a/docker/event-received/Dockerfile +++ b/docker/event-received/Dockerfile @@ -8,22 +8,22 @@ RUN go mod download COPY --link cmd/event-received ./cmd/event-received COPY --link internal ./internal -RUN GOOS=linux GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -tags lambda.norpc -o event-received ./cmd/event-received +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o event-received ./cmd/event-received FROM public.ecr.aws/lambda/provided:al2023.2024.10.14.12 AS dev WORKDIR /app COPY --from=build /app/event-received /var/task/event-received -COPY --link lang ./lang -COPY --link docker/event-received/aws-lambda-rie ./aws-lambda-rie +COPY --link lang /var/task/lang +COPY --link docker/aws-lambda-rie ./aws-lambda-rie ENTRYPOINT ["./event-received"] FROM public.ecr.aws/lambda/provided:al2023.2024.10.14.12 AS production WORKDIR /app -COPY --link docker/event-received/install_lambda_insights.sh /app/ +COPY --link docker/install_lambda_insights.sh /app/ RUN chmod +x /app/install_lambda_insights.sh \ && /app/install_lambda_insights.sh "${TARGETPLATFORM}" diff --git a/docker/event-received/install_lambda_insights.sh b/docker/install_lambda_insights.sh similarity index 100% rename from docker/event-received/install_lambda_insights.sh rename to docker/install_lambda_insights.sh diff --git a/docker/localstack/Dockerfile b/docker/localstack/Dockerfile index 768e510b7c..ccebddb0b4 100644 --- a/docker/localstack/Dockerfile +++ b/docker/localstack/Dockerfile @@ -8,15 +8,22 @@ COPY --link go.mod go.sum ./ RUN go mod download COPY --link cmd/event-received ./cmd/event-received +COPY --link cmd/schedule-runner ./cmd/schedule-runner COPY --link internal ./internal +COPY --link lang ./lang -RUN GOOS=linux GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -tags lambda.norpc -o event-received ./cmd/event-received \ - && zip event-received.zip event-received +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o cmd/event-received/bootstrap ./cmd/event-received \ + && chmod 755 cmd/event-received/bootstrap \ + && zip -j event-received.zip cmd/event-received/bootstrap && zip -r event-received.zip lang -FROM localstack/localstack:3.8.1 AS localstack +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o cmd/schedule-runner/bootstrap ./cmd/schedule-runner \ + && chmod 755 cmd/schedule-runner/bootstrap \ + && zip -j schedule-runner.zip cmd/schedule-runner/bootstrap && zip -r schedule-runner.zip lang +FROM --platform=${TARGETARCH} localstack/localstack:3.8.1 AS localstack COPY --from=build /app/event-received.zip /etc/event-received.zip +COPY --from=build /app/schedule-runner.zip /etc/schedule-runner.zip COPY ./docker/localstack/localstack-init.sh /etc/localstack/init/ready.d/localstack-init.sh diff --git a/docker/localstack/localstack-init.sh b/docker/localstack/localstack-init.sh index 32c1c7f18f..768e38bf13 100644 --- a/docker/localstack/localstack-init.sh +++ b/docker/localstack/localstack-init.sh @@ -32,12 +32,61 @@ awslocal s3api create-bucket --bucket evidence --create-bucket-configuration Loc echo 'configuring events' awslocal sqs create-queue --region eu-west-1 --queue-name event-queue awslocal events create-event-bus --region eu-west-1 --name default -awslocal events put-rule --region eu-west-1 --name send-events-to-queue-rule --event-bus-name default --event-pattern '{}' -awslocal events put-targets --region eu-west-1 --event-bus-name default --rule send-events-to-queue-rule --targets "Id"="event-queue","Arn"="arn:aws:sqs:eu-west-1:000000000000:event-queue" -echo 'creating lambda' -awslocal lambda create-function --environment Variables="{LPAS_TABLE=lpas,GOVUK_NOTIFY_IS_PRODUCTION=0,APP_PUBLIC_URL=localhost:5050,GOVUK_NOTIFY_BASE_URL=http://mock-notify:8080,UPLOADS_S3_BUCKET_NAME=evidence,UID_BASE_URL=http://mock-uid:8080,SEARCH_ENDPOINT=http://my-domain.eu-west-1.opensearch.localhost.localstack.cloud:4566,SEARCH_INDEXING_ENABLED=1}" --region eu-west-1 --function-name event-received --handler event-received --runtime go1.x --role arn:aws:iam::000000000000:role/lambda-role --zip-file fileb:///etc/event-received.zip +awslocal events put-rule \ + --region eu-west-1 \ + --name send-events-to-queue-rule \ + --event-bus-name default \ + --event-pattern '{}' + +awslocal events put-targets \ + --region eu-west-1 \ + --event-bus-name default \ + --rule send-events-to-queue-rule \ + --targets "Id"="event-queue","Arn"="arn:aws:sqs:eu-west-1:000000000000:event-queue" + +echo 'creating event-received lambda' +awslocal lambda create-function \ + --environment Variables="{LPAS_TABLE=lpas,GOVUK_NOTIFY_IS_PRODUCTION=0,APP_PUBLIC_URL=localhost:5050,GOVUK_NOTIFY_BASE_URL=http://mock-notify:8080,UPLOADS_S3_BUCKET_NAME=evidence,UID_BASE_URL=http://mock-uid:8080,SEARCH_ENDPOINT=http://my-domain.eu-west-1.opensearch.localhost.localstack.cloud:4566,SEARCH_INDEXING_ENABLED=1}" \ + --region eu-west-1 \ + --function-name event-received \ + --handler bootstrap \ + --runtime provided.al2023 \ + --role arn:aws:iam::000000000000:role/lambda-role \ + --zip-file fileb:///etc/event-received.zip + awslocal lambda wait function-active-v2 --region eu-west-1 --function-name event-received -awslocal events put-rule --region eu-west-1 --name receive-events-mlpa --event-bus-name default --event-pattern '{"source":["opg.poas.makeregister"],"detail-type":["uid-requested"]}' -awslocal events put-targets --region eu-west-1 --event-bus-name default --rule receive-events-mlpa --targets "Id"="receive-events-sirius","Arn"="arn:aws:lambda:eu-west-1:000000000000:function:event-received" +awslocal events put-rule \ + --region eu-west-1 \ + --name receive-events-mlpa \ + --event-bus-name default \ + --event-pattern '{"source":["opg.poas.makeregister"],"detail-type":["uid-requested"]}' + +awslocal events put-targets \ + --region eu-west-1 \ + --event-bus-name default \ + --rule receive-events-mlpa \ + --targets "Id"="receive-events-sirius","Arn"="arn:aws:lambda:eu-west-1:000000000000:function:event-received" + +echo 'creating schedule-runner lambda' +awslocal lambda create-function \ + --environment Variables="{LPAS_TABLE=lpas,GOVUK_NOTIFY_IS_PRODUCTION=0,GOVUK_NOTIFY_BASE_URL=http://mock-notify:8080,SEARCH_ENDPOINT=http://my-domain.eu-west-1.opensearch.localhost.localstack.cloud:4566,SEARCH_INDEXING_ENABLED=1,SEARCH_INDEX_NAME=lpas}" \ + --region eu-west-1 \ + --function-name schedule-runner \ + --handler bootstrap \ + --runtime provided.al2023 \ + --role arn:aws:iam::000000000000:role/lambda-role \ + --zip-file fileb:///etc/schedule-runner.zip \ + --timeout 900 + +awslocal lambda wait function-active-v2 --region eu-west-1 --function-name schedule-runner + +echo 'create and associate scheduler' +awslocal scheduler create-schedule \ + --region eu-west-1 \ + --name schedule-runner-minutely \ + --schedule-expression 'rate(1 minute)' \ + --description "Runs every minute (to aid testing - deployed infra will run less frequently)" \ + --target '{"RoleArn": "arn:aws:iam::000000000000:role/lambda-role", "Arn":"arn:aws:lambda:eu-west-1:000000000000:function:schedule-runner" }' \ + --flexible-time-window '{ "Mode": "OFF"}' diff --git a/docker/schedule-runner/.trivyignore.yaml b/docker/schedule-runner/.trivyignore.yaml new file mode 100644 index 0000000000..f9ce7c43ab --- /dev/null +++ b/docker/schedule-runner/.trivyignore.yaml @@ -0,0 +1,3 @@ +misconfigurations: + - id: AVD-DS-0002 + statement: Lambda creates a docker USER with least-privilege permissions. diff --git a/docker/schedule-runner/Dockerfile b/docker/schedule-runner/Dockerfile new file mode 100644 index 0000000000..8b5f076928 --- /dev/null +++ b/docker/schedule-runner/Dockerfile @@ -0,0 +1,34 @@ +FROM golang:1.23.1-alpine AS build + +WORKDIR /app + +COPY --link go.mod go.sum ./ +RUN go mod download + +COPY --link cmd/schedule-runner ./cmd/schedule-runner +COPY --link internal ./internal + +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o schedule-runner ./cmd/schedule-runner + +FROM public.ecr.aws/lambda/provided:al2023.2024.10.14.12 AS dev + +WORKDIR /app + +COPY --from=build /app/schedule-runner /var/task/schedule-runner +COPY --link lang /var/task/lang +COPY --link docker/aws-lambda-rie ./aws-lambda-rie + +ENTRYPOINT ["./schedule-runner"] + +FROM public.ecr.aws/lambda/provided:al2023.2024.10.14.12 AS production + +WORKDIR /app +COPY --link docker/install_lambda_insights.sh /app/ + +RUN chmod +x "/app/install_lambda_insights.sh" \ + && /app/install_lambda_insights.sh "${TARGETPLATFORM}" + +COPY --from=build /app/schedule-runner ./schedule-runner +COPY --link lang ./lang + +ENTRYPOINT ["./schedule-runner"] diff --git a/go.mod b/go.mod index 5431e4f044..a8699076f7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-lambda-go v1.47.0 github.com/aws/aws-sdk-go-v2 v1.32.2 github.com/aws/aws-sdk-go-v2/config v1.27.43 + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.2 @@ -44,7 +45,6 @@ require ( require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect diff --git a/go.sum b/go.sum index ea53694a15..6079e1190f 100644 --- a/go.sum +++ b/go.sum @@ -4,126 +4,54 @@ github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= -github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= -github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5/go.mod h1:wYSv6iDS621sEFLfKvpPE2ugjTuGlAG7iROg0hLOkfc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= -github.com/aws/aws-sdk-go-v2/config v1.27.39 h1:FCylu78eTGzW1ynHcongXK9YHtoXD5AiiUqq3YfJYjU= -github.com/aws/aws-sdk-go-v2/config v1.27.39/go.mod h1:wczj2hbyskP4LjMKBEZwPRO1shXY+GsQleab+ZXT2ik= -github.com/aws/aws-sdk-go-v2/config v1.27.40 h1:sie4mPBGFOO+Z27+yHzvyN31G20h/bf2xb5mCbpLv2Q= -github.com/aws/aws-sdk-go-v2/config v1.27.40/go.mod h1:4KW7Aa5tNo+0VHnuLnnE1vPHtwMurlNZNS65IdcewHA= github.com/aws/aws-sdk-go-v2/config v1.27.43 h1:p33fDDihFC390dhhuv8nOmX419wjOSDQRb+USt20RrU= github.com/aws/aws-sdk-go-v2/config v1.27.43/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu9vKEwxSEsGxihoW0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A= -github.com/aws/aws-sdk-go-v2/credentials v1.17.38 h1:iM90eRhCeZtlkzCNCG1JysOzJXGYf5rx80aD1lUgNDU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.38/go.mod h1:TCVYPZeQuLaYNEkf/TVn6k5k/zdVZZ7xH9po548VNNg= github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.8 h1:YNkm1DPhE4wnslPKD8jLVfKPujd94R8eI175vgKvIHI= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.8/go.mod h1:Ipgx7ZeodWz/Fd1TxCQwy0rXkxk2WDxZBJUuoZLzpqw= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.9 h1:Lu95fEezXH2rPkFP6iPBVcL5fmMJ9JHBmupSr17IgaI= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.9/go.mod h1:+LdH68eyAe4aTbjXW0THSPMHlemNUju27YOtI3Odwr8= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12 h1:zYf8E8zaqolHA5nQ+VmX2r3wc4K6xw5i6xKvvMjZBL0= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.12/go.mod h1:vYGIVLASk19Gb0FGwAcwES+qQF/aekD7m2G/X6mBOdQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 h1:OWYvKL53l1rbsUmW7bQyJVsYU/Ii3bbAAQIIFNbM0Tk= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18/go.mod h1:CUx0G1v3wG6l01tUB+j7Y8kclA8NSqK4ef0YG79a4cg= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.3 h1:X4iS+RcIKHkAMQz47nDt/nHxZUCKdnfgw940yluJ29Q= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.3/go.mod h1:k5XW8MoMxsNZ20RJmsokakvENUwQyjv69R9GqrI4xdQ= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.4 h1:W2rosR3B1RQb7Uy68AP3v034+ZBNDAhZ6sO+DXyBulI= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.4/go.mod h1:k5XW8MoMxsNZ20RJmsokakvENUwQyjv69R9GqrI4xdQ= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2 h1:kJqyYcGqhWFmXqjRrtFFD4Oc9FXiskhsll2xnlpe8Do= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.36.2/go.mod h1:+t2Zc5VNOzhaWzpGE+cEYZADsgAAQT5v55AO+fhU+2s= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.23.3 h1:q+pKQ9hZfIJNyoYSwPWbj19GnEPWvLOXwHpR/HYyx4o= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.23.3/go.mod h1:NZQWaOwOszI7jnQ7s1i5kN/FUAglaaJIm2htZG7BJKw= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.23.4 h1:V0R3pr1kCInKGnNk6yJ4FQdG14Xg2ZePNPtSMWWdrUI= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.23.4/go.mod h1:NZQWaOwOszI7jnQ7s1i5kN/FUAglaaJIm2htZG7BJKw= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.2 h1:E7Tuo0ipWpBl0f3uThz8cZsuyD5H8jLCnbtbKR4YL2s= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.2/go.mod h1:txOfweuNPBLhHodsV+C2lvPPRTommVTWbts9SZV6Myc= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.34.3 h1:voc3mmh8nP2y+XobELnq5ge7Om5FFJQ93AnTUTMwgUQ= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.34.3/go.mod h1:bcL34EfmexE+PLh2o4oC1VFpP82Ev8p4dL0PqdZ13dE= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.34.4 h1:PZTHZXIRGIkKc49vgDmhopmEIaensQG3jZ6rsY/uxRg= -github.com/aws/aws-sdk-go-v2/service/eventbridge v1.34.4/go.mod h1:bcL34EfmexE+PLh2o4oC1VFpP82Ev8p4dL0PqdZ13dE= github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.2 h1:FGrUiKglp0u7Zs19serLM/i22+IiwGxLCOJm4OtOMBI= github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.2/go.mod h1:OtWNmq2QGr/BUeJfs7ASAlzg0qjt96Su401dCdOks14= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 h1:rTWjG6AvWekO2B1LHeM3ktU7MqyX9rzWQ7hgzneZW7E= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20/go.mod h1:RGW2DDpVc8hu6Y6yG8G5CHVmVOAn1oV8rNKOHRJyswg= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.19 h1:dOxqOlOEa2e2heC/74+ZzcJOa27+F1aXFZpYgY/4QfA= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.19/go.mod h1:aV6U1beLFvk3qAgognjS3wnGGoDId8hlPEiBsLHXVZE= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.2 h1:1G7TTQNPNv5fhCyIQGYk8FOggLgkzKq6c4Y1nOGzAOE= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.2/go.mod h1:+ybYGLXoF7bcD7wIcMcklxyABZQmuBf1cHUhvY6FGIo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 h1:3zt8qqznMuAZWDTDpcwv9Xr11M/lVj2FsRR7oYBt0OA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q= -github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1 h1:jjHf+M6vCp/WzbyFEroY4/Nx8dJac520A0EPwlYk0Do= -github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q= github.com/aws/aws-sdk-go-v2/service/s3 v1.65.2 h1:yi8m+jepdp6foK14xXLGkYBenxnlcfJ45ka4Pg7fDSQ= github.com/aws/aws-sdk-go-v2/service/s3 v1.65.2/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3 h1:W2M3kQSuN1+FXgV2wMv1JMWPxw/37wBN87QHYDuTV0Y= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.3/go.mod h1:WyLS5qwXHtjKAONYZq/4ewdd+hcVsa3LBu77Ow5uj3k= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.4 h1:EoPbZg+DGTRqKKhwk5uDviV9yvx65r1kyoNNC02ZH4Y= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.33.4/go.mod h1:WyLS5qwXHtjKAONYZq/4ewdd+hcVsa3LBu77Ow5uj3k= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2 h1:Rrqru2wYkKQCS2IM5/JrgKUQIoNTqA6y/iuxkjzxC6M= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.2/go.mod h1:QuCURO98Sqee2AXmqDNxKXYFm2OEDAVAPApMqO0Vqnc= -github.com/aws/aws-sdk-go-v2/service/sqs v1.35.3 h1:Lcs658WFW235QuUfpAdxd8RCy8Va2VUA7/U9iIrcjcY= -github.com/aws/aws-sdk-go-v2/service/sqs v1.35.3/go.mod h1:WuGxWQhu2LXoPGA2HBIbotpwhM6T4hAz0Ip/HjdxfJg= -github.com/aws/aws-sdk-go-v2/service/sqs v1.35.4 h1:/tOrE92KXPF14vdIhwp/06zSYEKQMiwVtI4/qBugwDw= -github.com/aws/aws-sdk-go-v2/service/sqs v1.35.4/go.mod h1:WuGxWQhu2LXoPGA2HBIbotpwhM6T4hAz0Ip/HjdxfJg= github.com/aws/aws-sdk-go-v2/service/sqs v1.36.2 h1:kmbcoWgbzfh5a6rvfjOnfHSGEqD13qu1GfTPRZqg0FI= github.com/aws/aws-sdk-go-v2/service/sqs v1.36.2/go.mod h1:/UPx74a3M0WYeT2yLQYG/qHhkPlPXd6TsppfGgy2COk= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.4 h1:ck/Y8XWNR1gHa4BFkwE3oSu7XDJGwl+8TI7E/RB2EcQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.4/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4 h1:4f2/JKYZHAZbQ7koBpZ012bKi32NHPY0m7TDuJgsbug= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 h1:VzudTFrDCIDakXtemR7l6Qzt2+JYsVqo2MxBPt5k8T8= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.3/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.4 h1:uK6dUUdJtqutK1XO/tmNaQMJiPLCJY/eAeOOmqQ6ygY= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.4/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= -github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= -github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd h1:C0dfBzAdNMqxokqWUysk2KTJSMmqvh9cNW1opdy5+0Q= @@ -201,8 +129,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/ministryofjustice/opg-go-common v1.17.0 h1:c1LQZa4LjhQymVqzELWG8srWYmx0eXM3VnZTPeqwybM= -github.com/ministryofjustice/opg-go-common v1.17.0/go.mod h1:CtIVyfLfF+oeYS93n7su7u7/1ekqjthhwRbAsuy2b24= github.com/ministryofjustice/opg-go-common v1.18.0 h1:mgU2n3TpF8E7R5TXGQeQ/U2/+EfyU+rCMrdGkzf36x8= github.com/ministryofjustice/opg-go-common v1.18.0/go.mod h1:mtyOXWagT8HzXEJXMMH8WcL1bWUkBdpSBgebDskjM0c= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -264,8 +190,7 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/vektra/mockery/v2 v2.46.1 h1:lCR/zfGzZN9naZhyJryyj/8Iq2xgtyZIajlClGIebGA= -github.com/vektra/mockery/v2 v2.46.1/go.mod h1:dDivqi0ShM8A29mLgZn13yZ14MdXlTM4V360u8JDWCQ= +github.com/vektra/mockery/v2 v2.46.2 h1:bpUncWvkiDzqn+aWwt4dY1aS0F8Ob4k8+WJrWU/Kh4s= github.com/vektra/mockery/v2 v2.46.2/go.mod h1:dDivqi0ShM8A29mLgZn13yZ14MdXlTM4V360u8JDWCQ= github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= @@ -307,8 +232,6 @@ golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRj golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -317,31 +240,20 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= -google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/app/app.go b/internal/app/app.go index a0fb3a3125..addf804ba3 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,6 +53,7 @@ type DynamoClient interface { AllByPartialSK(ctx context.Context, pk dynamo.PK, partialSK dynamo.SK, v interface{}) error AllBySK(ctx context.Context, sk dynamo.SK, v interface{}) error AllKeysByPK(ctx context.Context, pk dynamo.PK) ([]dynamo.Keys, error) + AnyByPK(ctx context.Context, pk dynamo.PK, v interface{}) error BatchPut(ctx context.Context, items []interface{}) error Create(ctx context.Context, v interface{}) error DeleteKeys(ctx context.Context, keys []dynamo.Keys) error diff --git a/internal/app/mock_DynamoClient_test.go b/internal/app/mock_DynamoClient_test.go index b6ae48ab01..f5e997aac3 100644 --- a/internal/app/mock_DynamoClient_test.go +++ b/internal/app/mock_DynamoClient_test.go @@ -239,6 +239,54 @@ func (_c *mockDynamoClient_AllKeysByPK_Call) RunAndReturn(run func(context.Conte return _c } +// AnyByPK provides a mock function with given fields: ctx, pk, v +func (_m *mockDynamoClient) AnyByPK(ctx context.Context, pk dynamo.PK, v interface{}) error { + ret := _m.Called(ctx, pk, v) + + if len(ret) == 0 { + panic("no return value specified for AnyByPK") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, dynamo.PK, interface{}) error); ok { + r0 = rf(ctx, pk, v) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockDynamoClient_AnyByPK_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyByPK' +type mockDynamoClient_AnyByPK_Call struct { + *mock.Call +} + +// AnyByPK is a helper method to define mock.On call +// - ctx context.Context +// - pk dynamo.PK +// - v interface{} +func (_e *mockDynamoClient_Expecter) AnyByPK(ctx interface{}, pk interface{}, v interface{}) *mockDynamoClient_AnyByPK_Call { + return &mockDynamoClient_AnyByPK_Call{Call: _e.mock.On("AnyByPK", ctx, pk, v)} +} + +func (_c *mockDynamoClient_AnyByPK_Call) Run(run func(ctx context.Context, pk dynamo.PK, v interface{})) *mockDynamoClient_AnyByPK_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dynamo.PK), args[2].(interface{})) + }) + return _c +} + +func (_c *mockDynamoClient_AnyByPK_Call) Return(_a0 error) *mockDynamoClient_AnyByPK_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockDynamoClient_AnyByPK_Call) RunAndReturn(run func(context.Context, dynamo.PK, interface{}) error) *mockDynamoClient_AnyByPK_Call { + _c.Call.Return(run) + return _c +} + // BatchPut provides a mock function with given fields: ctx, items func (_m *mockDynamoClient) BatchPut(ctx context.Context, items []interface{}) error { ret := _m.Called(ctx, items) diff --git a/internal/dynamo/client.go b/internal/dynamo/client.go index 235338ead5..d00e10995b 100644 --- a/internal/dynamo/client.go +++ b/internal/dynamo/client.go @@ -471,3 +471,25 @@ func (c *Client) Move(ctx context.Context, oldKeys Keys, value any) error { return err } + +func (c *Client) AnyByPK(ctx context.Context, pk PK, v interface{}) error { + response, err := c.svc.Query(ctx, &dynamodb.QueryInput{ + TableName: aws.String(c.table), + ExpressionAttributeNames: map[string]string{"#PK": "PK"}, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":PK": &types.AttributeValueMemberS{Value: pk.PK()}, + }, + KeyConditionExpression: aws.String("#PK = :PK"), + Limit: aws.Int32(1), + }) + + if err != nil { + return err + } + + if len(response.Items) == 0 { + return NotFoundError{} + } + + return attributevalue.UnmarshalMap(response.Items[0], v) +} diff --git a/internal/dynamo/client_test.go b/internal/dynamo/client_test.go index ab6667e52f..d98e8edebb 100644 --- a/internal/dynamo/client_test.go +++ b/internal/dynamo/client_test.go @@ -1051,3 +1051,59 @@ func TestMoveWhenOtherCancellation(t *testing.T) { err := c.Move(ctx, Keys{PK: testPK("a-pk"), SK: testSK("an-sk")}, map[string]string{"hey": "hi"}) assert.Equal(t, canceledException, err) } + +func TestAnyByPK(t *testing.T) { + ctx := context.Background() + + expected := map[string]string{"Col": "Val"} + pkey, _ := attributevalue.Marshal("a-pk") + data, _ := attributevalue.MarshalMap(expected) + + dynamoDB := newMockDynamoDB(t) + dynamoDB.EXPECT(). + Query(ctx, &dynamodb.QueryInput{ + TableName: aws.String("this"), + ExpressionAttributeNames: map[string]string{"#PK": "PK"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":PK": pkey}, + KeyConditionExpression: aws.String("#PK = :PK"), + Limit: aws.Int32(1), + }). + Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{data}}, nil) + + c := &Client{table: "this", svc: dynamoDB} + + var v map[string]string + err := c.AnyByPK(ctx, testPK("a-pk"), &v) + assert.Nil(t, err) + assert.Equal(t, expected, v) +} + +func TestAnyByPKOnQueryError(t *testing.T) { + ctx := context.Background() + + dynamoDB := newMockDynamoDB(t) + dynamoDB.EXPECT(). + Query(ctx, mock.Anything). + Return(nil, expectedError) + + c := &Client{table: "this", svc: dynamoDB} + + var v map[string]string + err := c.AnyByPK(ctx, testPK("a-pk"), &v) + assert.Equal(t, expectedError, err) +} + +func TestAnyByPKWhenNotFound(t *testing.T) { + ctx := context.Background() + + dynamoDB := newMockDynamoDB(t) + dynamoDB.EXPECT(). + Query(ctx, mock.Anything). + Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{}}, nil) + + c := &Client{table: "this", svc: dynamoDB} + + var v map[string]string + err := c.AnyByPK(ctx, testPK("a-pk"), &v) + assert.Equal(t, NotFoundError{}, err) +} diff --git a/internal/notify/email.go b/internal/notify/email.go index aedec15e56..e243b034c8 100644 --- a/internal/notify/email.go +++ b/internal/notify/email.go @@ -271,7 +271,11 @@ func (e AttorneyOptedOutEmail) emailID(isProduction bool, _ localize.Lang) strin type DonorIdentityCheckExpiredEmail struct{} func (e DonorIdentityCheckExpiredEmail) emailID(isProduction bool, _ localize.Lang) string { - return "TODO" + if isProduction { + return "c3c4a115-4d07-4e25-926d-a656dc33485a" + } + + return "26509ca9-83d0-4417-ab5d-a3844916519e" } type VouchingShareCodeEmail struct { diff --git a/internal/scheduled/mock_DynamoClient_test.go b/internal/scheduled/mock_DynamoClient_test.go index 659d690407..37a85644dc 100644 --- a/internal/scheduled/mock_DynamoClient_test.go +++ b/internal/scheduled/mock_DynamoClient_test.go @@ -22,17 +22,17 @@ func (_m *mockDynamoClient) EXPECT() *mockDynamoClient_Expecter { return &mockDynamoClient_Expecter{mock: &_m.Mock} } -// Move provides a mock function with given fields: ctx, oldKeys, value -func (_m *mockDynamoClient) Move(ctx context.Context, oldKeys dynamo.Keys, value interface{}) error { - ret := _m.Called(ctx, oldKeys, value) +// AnyByPK provides a mock function with given fields: ctx, pk, v +func (_m *mockDynamoClient) AnyByPK(ctx context.Context, pk dynamo.PK, v interface{}) error { + ret := _m.Called(ctx, pk, v) if len(ret) == 0 { - panic("no return value specified for Move") + panic("no return value specified for AnyByPK") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, dynamo.Keys, interface{}) error); ok { - r0 = rf(ctx, oldKeys, value) + if rf, ok := ret.Get(0).(func(context.Context, dynamo.PK, interface{}) error); ok { + r0 = rf(ctx, pk, v) } else { r0 = ret.Error(0) } @@ -40,47 +40,47 @@ func (_m *mockDynamoClient) Move(ctx context.Context, oldKeys dynamo.Keys, value return r0 } -// mockDynamoClient_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move' -type mockDynamoClient_Move_Call struct { +// mockDynamoClient_AnyByPK_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyByPK' +type mockDynamoClient_AnyByPK_Call struct { *mock.Call } -// Move is a helper method to define mock.On call +// AnyByPK is a helper method to define mock.On call // - ctx context.Context -// - oldKeys dynamo.Keys -// - value interface{} -func (_e *mockDynamoClient_Expecter) Move(ctx interface{}, oldKeys interface{}, value interface{}) *mockDynamoClient_Move_Call { - return &mockDynamoClient_Move_Call{Call: _e.mock.On("Move", ctx, oldKeys, value)} +// - pk dynamo.PK +// - v interface{} +func (_e *mockDynamoClient_Expecter) AnyByPK(ctx interface{}, pk interface{}, v interface{}) *mockDynamoClient_AnyByPK_Call { + return &mockDynamoClient_AnyByPK_Call{Call: _e.mock.On("AnyByPK", ctx, pk, v)} } -func (_c *mockDynamoClient_Move_Call) Run(run func(ctx context.Context, oldKeys dynamo.Keys, value interface{})) *mockDynamoClient_Move_Call { +func (_c *mockDynamoClient_AnyByPK_Call) Run(run func(ctx context.Context, pk dynamo.PK, v interface{})) *mockDynamoClient_AnyByPK_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(dynamo.Keys), args[2].(interface{})) + run(args[0].(context.Context), args[1].(dynamo.PK), args[2].(interface{})) }) return _c } -func (_c *mockDynamoClient_Move_Call) Return(_a0 error) *mockDynamoClient_Move_Call { +func (_c *mockDynamoClient_AnyByPK_Call) Return(_a0 error) *mockDynamoClient_AnyByPK_Call { _c.Call.Return(_a0) return _c } -func (_c *mockDynamoClient_Move_Call) RunAndReturn(run func(context.Context, dynamo.Keys, interface{}) error) *mockDynamoClient_Move_Call { +func (_c *mockDynamoClient_AnyByPK_Call) RunAndReturn(run func(context.Context, dynamo.PK, interface{}) error) *mockDynamoClient_AnyByPK_Call { _c.Call.Return(run) return _c } -// OneByPK provides a mock function with given fields: ctx, pk, v -func (_m *mockDynamoClient) OneByPK(ctx context.Context, pk dynamo.PK, v interface{}) error { - ret := _m.Called(ctx, pk, v) +// Move provides a mock function with given fields: ctx, oldKeys, value +func (_m *mockDynamoClient) Move(ctx context.Context, oldKeys dynamo.Keys, value interface{}) error { + ret := _m.Called(ctx, oldKeys, value) if len(ret) == 0 { - panic("no return value specified for OneByPK") + panic("no return value specified for Move") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, dynamo.PK, interface{}) error); ok { - r0 = rf(ctx, pk, v) + if rf, ok := ret.Get(0).(func(context.Context, dynamo.Keys, interface{}) error); ok { + r0 = rf(ctx, oldKeys, value) } else { r0 = ret.Error(0) } @@ -88,32 +88,32 @@ func (_m *mockDynamoClient) OneByPK(ctx context.Context, pk dynamo.PK, v interfa return r0 } -// mockDynamoClient_OneByPK_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OneByPK' -type mockDynamoClient_OneByPK_Call struct { +// mockDynamoClient_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move' +type mockDynamoClient_Move_Call struct { *mock.Call } -// OneByPK is a helper method to define mock.On call +// Move is a helper method to define mock.On call // - ctx context.Context -// - pk dynamo.PK -// - v interface{} -func (_e *mockDynamoClient_Expecter) OneByPK(ctx interface{}, pk interface{}, v interface{}) *mockDynamoClient_OneByPK_Call { - return &mockDynamoClient_OneByPK_Call{Call: _e.mock.On("OneByPK", ctx, pk, v)} +// - oldKeys dynamo.Keys +// - value interface{} +func (_e *mockDynamoClient_Expecter) Move(ctx interface{}, oldKeys interface{}, value interface{}) *mockDynamoClient_Move_Call { + return &mockDynamoClient_Move_Call{Call: _e.mock.On("Move", ctx, oldKeys, value)} } -func (_c *mockDynamoClient_OneByPK_Call) Run(run func(ctx context.Context, pk dynamo.PK, v interface{})) *mockDynamoClient_OneByPK_Call { +func (_c *mockDynamoClient_Move_Call) Run(run func(ctx context.Context, oldKeys dynamo.Keys, value interface{})) *mockDynamoClient_Move_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(dynamo.PK), args[2].(interface{})) + run(args[0].(context.Context), args[1].(dynamo.Keys), args[2].(interface{})) }) return _c } -func (_c *mockDynamoClient_OneByPK_Call) Return(_a0 error) *mockDynamoClient_OneByPK_Call { +func (_c *mockDynamoClient_Move_Call) Return(_a0 error) *mockDynamoClient_Move_Call { _c.Call.Return(_a0) return _c } -func (_c *mockDynamoClient_OneByPK_Call) RunAndReturn(run func(context.Context, dynamo.PK, interface{}) error) *mockDynamoClient_OneByPK_Call { +func (_c *mockDynamoClient_Move_Call) RunAndReturn(run func(context.Context, dynamo.Keys, interface{}) error) *mockDynamoClient_Move_Call { _c.Call.Return(run) return _c } diff --git a/internal/scheduled/runner.go b/internal/scheduled/runner.go index 75c33fd243..eda348877e 100644 --- a/internal/scheduled/runner.go +++ b/internal/scheduled/runner.go @@ -47,19 +47,17 @@ type Runner struct { logger Logger store ScheduledStore now func() time.Time - period time.Duration donorStore DonorStore notifyClient NotifyClient actions map[Action]ActionFunc waiter Waiter } -func NewRunner(logger Logger, store ScheduledStore, donorStore DonorStore, notifyClient NotifyClient, period time.Duration) *Runner { +func NewRunner(logger Logger, store ScheduledStore, donorStore DonorStore, notifyClient NotifyClient) *Runner { r := &Runner{ logger: logger, store: store, now: time.Now, - period: period, donorStore: donorStore, notifyClient: notifyClient, waiter: &waiter{backoff: time.Second, sleep: time.Sleep, maxRetries: 10}, @@ -72,44 +70,22 @@ func NewRunner(logger Logger, store ScheduledStore, donorStore DonorStore, notif return r } -// Run the Runner, it is expected to be called in a Go routine. func (r *Runner) Run(ctx context.Context) error { - ticker := time.Tick(r.period) - - for { - innerCtx, cancel := context.WithTimeout(ctx, r.period) - defer cancel() - - r.logger.InfoContext(ctx, "runner step started") - if err := r.step(innerCtx); err != nil { - r.logger.ErrorContext(ctx, "runner step error", slog.Any("err", err)) - } - r.logger.InfoContext(ctx, "runner step finished") - - select { - case <-ctx.Done(): - return nil - case <-ticker: - continue - } - } -} - -func (r *Runner) step(ctx context.Context) error { r.waiter.Reset() for { row, err := r.store.Pop(ctx, r.now()) + if errors.Is(err, dynamo.NotFoundError{}) { + r.logger.InfoContext(ctx, "no scheduled tasks to process") return nil - } else if errors.Is(err, dynamo.ConditionalCheckFailedError{}) { - r.logger.InfoContext(ctx, "runner conditional check failed") + } else if err != nil { + r.logger.ErrorContext(ctx, "error getting scheduled task", slog.Any("err", err)) + if err := r.waiter.Wait(); err != nil { return err } continue - } else if err != nil { - return err } r.waiter.Reset() @@ -136,13 +112,6 @@ func (r *Runner) step(ctx context.Context) error { slog.String("target_sk", row.TargetLpaOwnerKey.SK())) } } - - select { - case <-ctx.Done(): - return nil - default: - continue - } } } diff --git a/internal/scheduled/runner_test.go b/internal/scheduled/runner_test.go index 6430f71b5c..ce38974cf9 100644 --- a/internal/scheduled/runner_test.go +++ b/internal/scheduled/runner_test.go @@ -22,13 +22,6 @@ var ( expectedError = errors.New("hey") testNow = time.Now() testNowFn = func() time.Time { return testNow } - - // set resolution lower to make tests more accurate, but the clock won't be - // perfect so 3ms seems a reasonable trade-off - resolution = 3 * time.Millisecond - // set period higher to make tests more accurate, but that will make them - // slower - period = 20 * resolution ) func (m *mockScheduledStore) ExpectPops(returns ...any) { @@ -53,47 +46,15 @@ func TestNewRunner(t *testing.T) { donorStore := newMockDonorStore(t) notifyClient := newMockNotifyClient(t) - runner := NewRunner(logger, store, donorStore, notifyClient, time.Hour) + runner := NewRunner(logger, store, donorStore, notifyClient) assert.Equal(t, logger, runner.logger) assert.Equal(t, store, runner.store) assert.Equal(t, donorStore, runner.donorStore) assert.Equal(t, notifyClient, runner.notifyClient) - assert.Equal(t, time.Hour, runner.period) } func TestRunnerRun(t *testing.T) { - ctx, _ := context.WithTimeout(ctx, period) - - logger := newMockLogger(t) - logger.EXPECT(). - InfoContext(ctx, "runner step started", mock.Anything) - logger.EXPECT(). - InfoContext(ctx, "runner step finished", mock.Anything) - - store := newMockScheduledStore(t) - store.EXPECT(). - Pop(mock.Anything, testNow). - Return(nil, dynamo.NotFoundError{}). - Once() - - waiter := newMockWaiter(t) - waiter.EXPECT().Reset() - - runner := &Runner{ - now: testNowFn, - period: time.Hour, - logger: logger, - store: store, - waiter: waiter, - } - - err := runner.Run(ctx) - assert.Nil(t, err) -} - -func TestRunnerRunWhenPeriodElapses(t *testing.T) { - ctx, cancel := context.WithTimeout(ctx, 3*period) event := &Event{ Action: 99, TargetLpaKey: dynamo.LpaKey("an-lpa"), @@ -102,59 +63,50 @@ func TestRunnerRunWhenPeriodElapses(t *testing.T) { logger := newMockLogger(t) logger.EXPECT(). - InfoContext(ctx, "runner step started", mock.Anything) - logger.EXPECT(). - InfoContext(ctx, "runner step finished", mock.Anything) + InfoContext(ctx, "runner action", slog.String("action", "Action(99)")) logger.EXPECT(). - InfoContext(mock.Anything, "runner action", mock.Anything) + InfoContext(ctx, "runner action success", + slog.String("action", "Action(99)"), + slog.String("target_pk", "LPA#an-lpa"), + slog.String("target_sk", "DONOR#a-donor")) logger.EXPECT(). - InfoContext(mock.Anything, "runner action success", mock.Anything, mock.Anything, mock.Anything) + InfoContext(ctx, "no scheduled tasks to process") store := newMockScheduledStore(t) - store.ExpectPops( - event, nil, - nil, dynamo.NotFoundError{}, - event, nil, - nil, dynamo.NotFoundError{}, - event, nil) + store.EXPECT(). + Pop(ctx, testNow). + Return(event, nil). + Once() + store.EXPECT(). + Pop(ctx, testNow). + Return(nil, dynamo.NotFoundError{}). + Once() waiter := newMockWaiter(t) waiter.EXPECT().Reset() - var runTimes []time.Time + actionFunc := newMockActionFunc(t) + actionFunc.EXPECT(). + Execute(ctx, event). + Return(nil) + runner := &Runner{ - now: time.Now, - period: period, + now: testNowFn, logger: logger, store: store, waiter: waiter, actions: map[Action]ActionFunc{ - Action(99): func(_ context.Context, _ *Event) error { - if runTimes = append(runTimes, time.Now()); len(runTimes) == 3 { - cancel() - } - return nil - }, + 99: actionFunc.Execute, }, } - err := runner.Run(ctx) assert.Nil(t, err) - assert.Len(t, runTimes, 3) - assert.InDelta(t, period, runTimes[1].Sub(runTimes[0]), float64(resolution)) - assert.InDelta(t, period, runTimes[2].Sub(runTimes[1]), float64(resolution)) } func TestRunnerRunWhenStepErrors(t *testing.T) { - ctx, _ := context.WithTimeout(ctx, period) - logger := newMockLogger(t) logger.EXPECT(). - InfoContext(ctx, "runner step started", mock.Anything) - logger.EXPECT(). - InfoContext(ctx, "runner step finished", mock.Anything) - logger.EXPECT(). - ErrorContext(ctx, "runner step error", slog.Any("err", expectedError)) + ErrorContext(ctx, "error getting scheduled task", slog.Any("err", expectedError)) store := newMockScheduledStore(t) store.EXPECT(). @@ -164,67 +116,22 @@ func TestRunnerRunWhenStepErrors(t *testing.T) { waiter := newMockWaiter(t) waiter.EXPECT().Reset() + waiter.EXPECT(). + Wait(). + Return(expectedError) runner := &Runner{ now: testNowFn, - period: time.Hour, logger: logger, store: store, waiter: waiter, } err := runner.Run(ctx) - assert.Nil(t, err) -} - -func TestRunnerStep(t *testing.T) { - event := &Event{ - Action: 99, - TargetLpaKey: dynamo.LpaKey("an-lpa"), - TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), - } - - logger := newMockLogger(t) - logger.EXPECT(). - InfoContext(ctx, "runner action", slog.String("action", "Action(99)")) - logger.EXPECT(). - InfoContext(ctx, "runner action success", - slog.String("action", "Action(99)"), - slog.String("target_pk", "LPA#an-lpa"), - slog.String("target_sk", "DONOR#a-donor")) - - store := newMockScheduledStore(t) - store.EXPECT(). - Pop(ctx, testNow). - Return(event, nil). - Once() - store.EXPECT(). - Pop(ctx, testNow). - Return(nil, dynamo.NotFoundError{}). - Once() - - waiter := newMockWaiter(t) - waiter.EXPECT().Reset() - - actionFunc := newMockActionFunc(t) - actionFunc.EXPECT(). - Execute(ctx, event). - Return(nil) - - runner := &Runner{ - now: testNowFn, - logger: logger, - store: store, - waiter: waiter, - actions: map[Action]ActionFunc{ - 99: actionFunc.Execute, - }, - } - err := runner.step(ctx) - assert.Nil(t, err) + assert.Equal(t, expectedError, err) } -func TestRunnerStepWhenActionIgnored(t *testing.T) { +func TestRunnerRunWhenActionIgnored(t *testing.T) { event := &Event{ Action: 99, TargetLpaKey: dynamo.LpaKey("an-lpa"), @@ -239,6 +146,8 @@ func TestRunnerStepWhenActionIgnored(t *testing.T) { slog.String("action", "Action(99)"), slog.String("target_pk", "LPA#an-lpa"), slog.String("target_sk", "DONOR#a-donor")) + logger.EXPECT(). + InfoContext(ctx, "no scheduled tasks to process") store := newMockScheduledStore(t) store.EXPECT(). @@ -267,11 +176,11 @@ func TestRunnerStepWhenActionIgnored(t *testing.T) { 99: actionFunc.Execute, }, } - err := runner.step(ctx) + err := runner.Run(ctx) assert.Nil(t, err) } -func TestRunnerStepWhenActionErrors(t *testing.T) { +func TestRunnerRunWhenActionErrors(t *testing.T) { event := &Event{ Action: 99, TargetLpaKey: dynamo.LpaKey("an-lpa"), @@ -287,6 +196,8 @@ func TestRunnerStepWhenActionErrors(t *testing.T) { slog.String("target_pk", "LPA#an-lpa"), slog.String("target_sk", "DONOR#a-donor"), slog.Any("err", expectedError)) + logger.EXPECT(). + InfoContext(ctx, "no scheduled tasks to process") store := newMockScheduledStore(t) store.EXPECT(). @@ -315,61 +226,68 @@ func TestRunnerStepWhenActionErrors(t *testing.T) { 99: actionFunc.Execute, }, } - err := runner.step(ctx) + err := runner.Run(ctx) assert.Nil(t, err) } -func TestRunnerStepWhenConditionalCheckFails(t *testing.T) { - event := &Event{ - Action: 99, - TargetLpaKey: dynamo.LpaKey("an-lpa"), - TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), +func TestRunnerRunWhenWaitingError(t *testing.T) { + testcases := []error{ + dynamo.ConditionalCheckFailedError{}, + expectedError, } - logger := newMockLogger(t) - logger.EXPECT(). - InfoContext(ctx, "runner action", slog.String("action", "Action(99)")) - logger.EXPECT(). - InfoContext(ctx, "runner conditional check failed") - logger.EXPECT(). - InfoContext(ctx, "runner action success", - slog.String("action", "Action(99)"), - slog.String("target_pk", "LPA#an-lpa"), - slog.String("target_sk", "DONOR#a-donor")) - - store := newMockScheduledStore(t) - store.ExpectPops( - nil, dynamo.ConditionalCheckFailedError{}, - event, nil, - nil, dynamo.NotFoundError{}) - - waiter := newMockWaiter(t) - waiter.EXPECT().Reset().Twice() - waiter.EXPECT().Wait().Return(nil).Once() + for _, waitingError := range testcases { + t.Run(waitingError.Error(), func(t *testing.T) { + event := &Event{ + Action: 99, + TargetLpaKey: dynamo.LpaKey("an-lpa"), + TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), + } - actionFunc := newMockActionFunc(t) - actionFunc.EXPECT(). - Execute(mock.Anything, mock.Anything). - Return(nil) + logger := newMockLogger(t) + logger.EXPECT(). + InfoContext(ctx, "runner action", slog.String("action", "Action(99)")) + logger.EXPECT(). + ErrorContext(ctx, "error getting scheduled task", slog.Any("err", waitingError)) + logger.EXPECT(). + InfoContext(ctx, "runner action success", + slog.String("action", "Action(99)"), + slog.String("target_pk", "LPA#an-lpa"), + slog.String("target_sk", "DONOR#a-donor")) + logger.EXPECT(). + InfoContext(ctx, "no scheduled tasks to process") + + store := newMockScheduledStore(t) + store.ExpectPops( + nil, waitingError, + event, nil, + nil, dynamo.NotFoundError{}) + + waiter := newMockWaiter(t) + waiter.EXPECT().Reset().Twice() + waiter.EXPECT().Wait().Return(nil).Once() + + actionFunc := newMockActionFunc(t) + actionFunc.EXPECT(). + Execute(mock.Anything, mock.Anything). + Return(nil) - runner := &Runner{ - now: testNowFn, - logger: logger, - store: store, - waiter: waiter, - actions: map[Action]ActionFunc{ - 99: actionFunc.Execute, - }, + runner := &Runner{ + now: testNowFn, + logger: logger, + store: store, + waiter: waiter, + actions: map[Action]ActionFunc{ + 99: actionFunc.Execute, + }, + } + err := runner.Run(ctx) + assert.Nil(t, err) + }) } - err := runner.step(ctx) - assert.Nil(t, err) } -func TestRunnerStepWhenConditionalCheckFailsAndWaiterErrors(t *testing.T) { - logger := newMockLogger(t) - logger.EXPECT(). - InfoContext(ctx, "runner conditional check failed") - +func TestRunnerRunWhenConditionalCheckFailsAndWaiterErrors(t *testing.T) { store := newMockScheduledStore(t) store.ExpectPops( nil, dynamo.ConditionalCheckFailedError{}, @@ -380,17 +298,21 @@ func TestRunnerStepWhenConditionalCheckFailsAndWaiterErrors(t *testing.T) { waiter.EXPECT().Wait().Return(nil).Once() waiter.EXPECT().Wait().Return(expectedError).Once() + logger := newMockLogger(t) + logger.EXPECT(). + ErrorContext(ctx, "error getting scheduled task", slog.Any("err", dynamo.ConditionalCheckFailedError{})) + runner := &Runner{ now: testNowFn, - logger: logger, store: store, waiter: waiter, + logger: logger, } - err := runner.step(ctx) + err := runner.Run(ctx) assert.Equal(t, expectedError, err) } -func TestRunnerStepCancelDonorIdentity(t *testing.T) { +func TestRunnerCancelDonorIdentity(t *testing.T) { lpaKey := dynamo.LpaKey("an-lpa") donorKey := dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")) event := &Event{ @@ -429,7 +351,7 @@ func TestRunnerStepCancelDonorIdentity(t *testing.T) { assert.Nil(t, err) } -func TestRunnerStepCancelDonorIdentityWhenDonorStoreErrors(t *testing.T) { +func TestRunnerCancelDonorIdentityWhenDonorStoreErrors(t *testing.T) { event := &Event{ TargetLpaKey: dynamo.LpaKey("an-lpa"), TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), @@ -448,12 +370,12 @@ func TestRunnerStepCancelDonorIdentityWhenDonorStoreErrors(t *testing.T) { assert.ErrorContains(t, err, "error retrieving donor: hey") } -func TestRunnerStepCancelDonorIdentityWhenStepIgnored(t *testing.T) { +func TestRunnerCancelDonorIdentityWhenStepIgnored(t *testing.T) { testcases := map[string]*donordata.Provided{ - "identity not confirmed": &donordata.Provided{ + "identity not confirmed": { IdentityUserData: identity.UserData{Status: identity.StatusFailed}, }, - "already signed": &donordata.Provided{ + "already signed": { IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, SignedAt: time.Now(), }, @@ -483,7 +405,7 @@ func TestRunnerStepCancelDonorIdentityWhenStepIgnored(t *testing.T) { } } -func TestRunnerStepCancelDonorIdentityWhenNotifySendErrors(t *testing.T) { +func TestRunnerCancelDonorIdentityWhenNotifySendErrors(t *testing.T) { event := &Event{ TargetLpaKey: dynamo.LpaKey("an-lpa"), TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), @@ -512,7 +434,7 @@ func TestRunnerStepCancelDonorIdentityWhenNotifySendErrors(t *testing.T) { assert.ErrorIs(t, err, expectedError) } -func TestRunnerStepCancelDonorIdentityWhenDonorStorePutErrors(t *testing.T) { +func TestRunnerCancelDonorIdentityWhenDonorStorePutErrors(t *testing.T) { event := &Event{ TargetLpaKey: dynamo.LpaKey("an-lpa"), TargetLpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("a-donor")), diff --git a/internal/scheduled/store.go b/internal/scheduled/store.go index 179309ba7b..c2c1e0f3f0 100644 --- a/internal/scheduled/store.go +++ b/internal/scheduled/store.go @@ -9,7 +9,7 @@ import ( type DynamoClient interface { Move(ctx context.Context, oldKeys dynamo.Keys, value any) error - OneByPK(ctx context.Context, pk dynamo.PK, v interface{}) error + AnyByPK(ctx context.Context, pk dynamo.PK, v interface{}) error Put(ctx context.Context, v interface{}) error } @@ -27,7 +27,7 @@ func NewStore(dynamoClient DynamoClient) *Store { func (s *Store) Pop(ctx context.Context, day time.Time) (*Event, error) { var row Event - if err := s.dynamoClient.OneByPK(ctx, dynamo.ScheduledDayKey(day), &row); err != nil { + if err := s.dynamoClient.AnyByPK(ctx, dynamo.ScheduledDayKey(day), &row); err != nil { return nil, err } diff --git a/internal/scheduled/store_test.go b/internal/scheduled/store_test.go index a7268ace0f..2230700dd7 100644 --- a/internal/scheduled/store_test.go +++ b/internal/scheduled/store_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/mock" ) -func (c *mockDynamoClient_OneByPK_Call) SetData(row *Event) { +func (c *mockDynamoClient_AnyByPK_Call) SetData(row *Event) { c.Run(func(_ context.Context, _ dynamo.PK, v any) { b, _ := attributevalue.Marshal(row) attributevalue.Unmarshal(b, v) @@ -42,7 +42,7 @@ func TestStorePop(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient.EXPECT(). - OneByPK(ctx, dynamo.ScheduledDayKey(testNow), mock.Anything). + AnyByPK(ctx, dynamo.ScheduledDayKey(testNow), mock.Anything). Return(nil). SetData(row) dynamoClient.EXPECT(). @@ -55,10 +55,10 @@ func TestStorePop(t *testing.T) { assert.Equal(t, movedRow, result) } -func TestStorePopWhenOneByPKErrors(t *testing.T) { +func TestStorePopWhenAnyByPKErrors(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient.EXPECT(). - OneByPK(mock.Anything, mock.Anything, mock.Anything). + AnyByPK(mock.Anything, mock.Anything, mock.Anything). Return(expectedError) store := &Store{dynamoClient: dynamoClient} @@ -69,7 +69,7 @@ func TestStorePopWhenOneByPKErrors(t *testing.T) { func TestStorePopWhenDeleteOneErrors(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient.EXPECT(). - OneByPK(mock.Anything, mock.Anything, mock.Anything). + AnyByPK(mock.Anything, mock.Anything, mock.Anything). Return(nil). SetData(&Event{ Action: 99, diff --git a/scripts/add-scheduled-items.go b/scripts/add-scheduled-items.go new file mode 100644 index 0000000000..45b4216534 --- /dev/null +++ b/scripts/add-scheduled-items.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/google/uuid" + "github.com/ministryofjustice/opg-modernising-lpa/internal/donor/donordata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/ministryofjustice/opg-modernising-lpa/internal/scheduled" +) + +func main() { + ctx := context.Background() + + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("eu-west-1"), + config.WithEndpointResolver(aws.EndpointResolverFunc( + func(service, region string) (aws.Endpoint, error) { + return aws.Endpoint{ + URL: "http://localhost:4566", + }, nil + }, + )), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + "test", + "test", + "test", + )), + ) + + if err != nil { + log.Fatal("failed to load default config: %w", err) + } + + client, err := dynamo.NewClient(cfg, "lpas") + if err != nil { + log.Fatal("failed to create dynamo client: %w", err) + } + + var items []any + now := time.Now() + + for i := 0; i < 10_000; i++ { + now = now.Add(time.Second * 1) + + donor := &donordata.Provided{ + LpaUID: uuid.NewString(), + PK: dynamo.LpaKey(uuid.NewString()), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey(uuid.NewString())), + IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, + Donor: donordata.Donor{Email: "a@b.com"}, + } + + event := scheduled.Event{ + At: now, + Action: scheduled.ActionExpireDonorIdentity, + TargetLpaKey: donor.PK, + TargetLpaOwnerKey: donor.SK, + PK: dynamo.ScheduledDayKey(now), + SK: dynamo.ScheduledKey(now, int(scheduled.ActionExpireDonorIdentity)), + } + + items = append(items, donor, event) + + if len(items) == 100 { + if err := client.BatchPut(ctx, items); err != nil { + log.Fatal(err) + } + + items = []any{} + } + } +} diff --git a/terraform/environment/global/iam_schedule_runner_lambda_role.tf b/terraform/environment/global/iam_schedule_runner_lambda_role.tf new file mode 100644 index 0000000000..bf4e9372b2 --- /dev/null +++ b/terraform/environment/global/iam_schedule_runner_lambda_role.tf @@ -0,0 +1,8 @@ +resource "aws_iam_role" "schedule_runner_lambda" { + name = "schedule-runner-${data.aws_default_tags.current.tags.environment-name}" + assume_role_policy = data.aws_iam_policy_document.lambda_assume.json + lifecycle { + create_before_destroy = true + } + provider = aws.global +} diff --git a/terraform/environment/global/iam_schedule_runner_scheduler.tf b/terraform/environment/global/iam_schedule_runner_scheduler.tf new file mode 100644 index 0000000000..720ca1214e --- /dev/null +++ b/terraform/environment/global/iam_schedule_runner_scheduler.tf @@ -0,0 +1,18 @@ +resource "aws_iam_role" "schedule_runner_scheduler" { + name = "${data.aws_default_tags.current.tags.environment-name}-scheduler-role" + assume_role_policy = data.aws_iam_policy_document.schedule_runner_scheduler_assume.json + provider = aws.global +} + +data "aws_iam_policy_document" "schedule_runner_scheduler_assume" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + identifiers = ["scheduler.amazonaws.com"] + type = "Service" + } + } + provider = aws.global +} diff --git a/terraform/environment/global/outputs.tf b/terraform/environment/global/outputs.tf index a271619caa..c9358cb249 100644 --- a/terraform/environment/global/outputs.tf +++ b/terraform/environment/global/outputs.tf @@ -11,6 +11,8 @@ output "iam_roles" { fault_injection_simulator = aws_iam_role.fault_injection_simulator, create_s3_batch_replication_jobs_lambda = aws_iam_role.create_s3_batch_replication_jobs_lambda event_received_lambda = aws_iam_role.event_received_lambda + schedule_runner_lambda = aws_iam_role.schedule_runner_lambda opensearch_pipeline = aws_iam_role.opensearch_pipeline + schedule_runner_scheduler = aws_iam_role.schedule_runner_scheduler } } diff --git a/terraform/environment/region/modules/schedule_runner/data_sources.tf b/terraform/environment/region/modules/schedule_runner/data_sources.tf new file mode 100644 index 0000000000..a4470e95c7 --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/data_sources.tf @@ -0,0 +1,21 @@ +data "aws_default_tags" "current" { + provider = aws.region +} + +data "aws_region" "current" { + provider = aws.region +} + +data "aws_caller_identity" "current" { + provider = aws.region +} + +data "aws_kms_alias" "cloudwatch_application_logs_encryption" { + name = "alias/${data.aws_default_tags.current.tags.application}_cloudwatch_application_logs_encryption" + provider = aws.region +} + +data "aws_secretsmanager_secret" "gov_uk_notify_api_key" { + name = "gov-uk-notify-api-key" + provider = aws.region +} diff --git a/terraform/environment/region/modules/schedule_runner/lambda.tf b/terraform/environment/region/modules/schedule_runner/lambda.tf new file mode 100644 index 0000000000..e0fccfb9fd --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/lambda.tf @@ -0,0 +1,150 @@ +module "schedule_runner" { + source = "../lambda" + lambda_name = "schedule-runner" + description = "Function to run scheduled tasks on a schedule set by EventBridge Scheduler" + environment_variables = { + EVENT_BUS_NAME = var.event_bus_name + GOVUK_NOTIFY_BASE_URL = "https://api.notifications.service.gov.uk" + GOVUK_NOTIFY_IS_PRODUCTION = data.aws_default_tags.current.tags.environment-name == "production" ? "1" : "0" + LPAS_TABLE = var.lpas_table.name + SEARCH_ENDPOINT = var.search_endpoint + SEARCH_INDEX_NAME = var.search_index_name + SEARCH_INDEXING_DISABLED = 1 + } + image_uri = "${var.lambda_function_image_ecr_url}:${var.lambda_function_image_tag}" + aws_iam_role = var.schedule_runner_lambda_role + environment = data.aws_default_tags.current.tags.environment-name + kms_key = data.aws_kms_alias.cloudwatch_application_logs_encryption.target_key_arn + iam_policy_documents = [data.aws_iam_policy_document.schedule_runner.json] + timeout = 900 + memory = 1024 + vpc_config = { + subnet_ids = var.vpc_config.subnet_ids + security_group_ids = var.vpc_config.security_group_ids + } + providers = { + aws.region = aws.region + } +} + +resource "aws_scheduler_schedule" "schedule_runner_hourly" { + name = "schedule-runner-hourly" + schedule_expression = "rate(1 hour)" + description = "Runs every hour" + + flexible_time_window { + mode = "OFF" + } + + target { + arn = module.schedule_runner.lambda.arn + role_arn = var.schedule_runner_scheduler.arn + } + + provider = aws.region +} + +data "aws_iam_policy_document" "schedule_runner" { + statement { + sid = "${local.policy_region_prefix}DynamoDBEncryptionAccess" + effect = "Allow" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:RetireGrant", + ] + + resources = [ + data.aws_kms_alias.dynamodb_encryption_key.target_key_arn, + ] + } + + statement { + sid = "${local.policy_region_prefix}Allow" + + actions = [ + "dynamodb:DeleteItem", + "dynamodb:PutItem", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:UpdateItem", + ] + + resources = [ + var.lpas_table.arn, + "${var.lpas_table.arn}/index/*", + ] + } + + statement { + sid = "${local.policy_region_prefix}SecretAccess" + effect = "Allow" + + actions = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + ] + + resources = [ + data.aws_secretsmanager_secret.gov_uk_notify_api_key.arn, + ] + } + + statement { + effect = "Allow" + + resources = [ + data.aws_kms_alias.secrets_manager_secret_encryption_key.target_key_arn, + data.aws_kms_alias.aws_lambda.target_key_arn, + ] + + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:DescribeKey", + ] + } + + provider = aws.region +} + +resource "aws_iam_role_policy" "schedule_runner" { + name = "schedule_runner-${data.aws_default_tags.current.tags.environment-name}" + role = var.schedule_runner_lambda_role.id + policy = data.aws_iam_policy_document.schedule_runner.json + provider = aws.region +} + +data "aws_kms_alias" "dynamodb_encryption_key" { + name = "alias/${data.aws_default_tags.current.tags.application}_dynamodb_encryption" + provider = aws.region +} + +data "aws_kms_alias" "secrets_manager_secret_encryption_key" { + name = "alias/${data.aws_default_tags.current.tags.application}_secrets_manager_secret_encryption_key" + provider = aws.region +} + +data "aws_kms_alias" "aws_lambda" { + name = "alias/aws/lambda" + provider = aws.region +} + +resource "aws_lambda_permission" "allow_cloudwatch_scheduler_to_call_schedule_runner" { + statement_id = "AllowExecutionFromCloudWatchMlpa" + action = "lambda:InvokeFunction" + function_name = module.schedule_runner.lambda.function_name + principal = "events.amazonaws.com" + source_account = data.aws_caller_identity.current.account_id + source_arn = aws_scheduler_schedule.schedule_runner_hourly.arn + provider = aws.region +} + +locals { + policy_region_prefix = lower(replace(data.aws_region.current.name, "-", "")) +} diff --git a/terraform/environment/region/modules/schedule_runner/outputs.tf b/terraform/environment/region/modules/schedule_runner/outputs.tf new file mode 100644 index 0000000000..82f508adfa --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/outputs.tf @@ -0,0 +1,3 @@ +output "lambda_function" { + value = module.schedule_runner.lambda +} diff --git a/terraform/environment/region/modules/schedule_runner/scheduler_iam.tf b/terraform/environment/region/modules/schedule_runner/scheduler_iam.tf new file mode 100644 index 0000000000..d5d1e6a4ff --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/scheduler_iam.tf @@ -0,0 +1,17 @@ +data "aws_iam_policy_document" "lambda_access_policy" { + statement { + sid = "allowLambdaInvoke" + effect = "Allow" + resources = [module.schedule_runner.lambda.arn] + actions = [ + "lambda:InvokeFunction", + ] + } + provider = aws.region +} + +resource "aws_iam_role_policy" "lambda_access_role_policy" { + policy = data.aws_iam_policy_document.lambda_access_policy.json + role = var.schedule_runner_scheduler.name + provider = aws.region +} diff --git a/terraform/environment/region/modules/schedule_runner/variables.tf b/terraform/environment/region/modules/schedule_runner/variables.tf new file mode 100644 index 0000000000..9f159f26bc --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/variables.tf @@ -0,0 +1,45 @@ +variable "lambda_function_image_ecr_url" { + type = string +} + +variable "lambda_function_image_tag" { + type = string +} + +variable "event_bus_name" { + type = string +} + +variable "schedule_runner_lambda_role" { + type = any +} + +variable "vpc_config" { + description = "Configuration block for VPC" + type = object({ + subnet_ids = list(string) + security_group_ids = list(string) + }) +} + +variable "lpas_table" { + type = object({ + arn = string + name = string + }) +} + +variable "search_endpoint" { + type = string + description = "URL of the OpenSearch Service endpoint to use" +} + +variable "search_index_name" { + type = string + description = "Name of the OpenSearch Service index to use" +} + +variable "schedule_runner_scheduler" { + description = "IAM role for AWS schedule runner EventBridge Scheduler" + type = any +} diff --git a/terraform/environment/region/modules/schedule_runner/versions.tf b/terraform/environment/region/modules/schedule_runner/versions.tf new file mode 100644 index 0000000000..420cbae620 --- /dev/null +++ b/terraform/environment/region/modules/schedule_runner/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5.2" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.70.0" + configuration_aliases = [ + aws.region, + aws.management + ] + } + } +} diff --git a/terraform/environment/region/schedule_runner.tf b/terraform/environment/region/schedule_runner.tf new file mode 100644 index 0000000000..90ba311abb --- /dev/null +++ b/terraform/environment/region/schedule_runner.tf @@ -0,0 +1,29 @@ +data "aws_ecr_repository" "schedule_runner" { + name = "modernising-lpa/schedule-runner" + provider = aws.management +} + +module "schedule_runner" { + source = "./modules/schedule_runner" + lambda_function_image_ecr_url = data.aws_ecr_repository.schedule_runner.repository_url + lambda_function_image_tag = var.app_service_container_version + event_bus_name = module.event_bus.event_bus.name + search_endpoint = var.search_endpoint + search_index_name = var.search_index_name + schedule_runner_scheduler = var.iam_roles.schedule_runner_scheduler + schedule_runner_lambda_role = var.iam_roles.schedule_runner_lambda + vpc_config = { + subnet_ids = data.aws_subnet.application[*].id + security_group_ids = [data.aws_security_group.lambda_egress.id] + } + + lpas_table = { + arn = var.lpas_table.arn + name = var.lpas_table.name + } + + providers = { + aws.region = aws.region + aws.management = aws.management + } +} diff --git a/terraform/environment/region/variables.tf b/terraform/environment/region/variables.tf index 14c92df5d6..8f2934ad92 100644 --- a/terraform/environment/region/variables.tf +++ b/terraform/environment/region/variables.tf @@ -11,6 +11,8 @@ variable "iam_roles" { fault_injection_simulator = any create_s3_batch_replication_jobs_lambda = any event_received_lambda = any + schedule_runner_scheduler = any + schedule_runner_lambda = any }) description = "ARN of IAM role that allows your Amazon ECS container task to make calls to other AWS services." } diff --git a/terraform/environment/regions.tf b/terraform/environment/regions.tf index 6d0e30e789..a19a5d577d 100644 --- a/terraform/environment/regions.tf +++ b/terraform/environment/regions.tf @@ -34,6 +34,8 @@ module "eu_west_1" { fault_injection_simulator = module.global.iam_roles.fault_injection_simulator create_s3_batch_replication_jobs_lambda = module.global.iam_roles.create_s3_batch_replication_jobs_lambda event_received_lambda = module.global.iam_roles.event_received_lambda + schedule_runner_lambda = module.global.iam_roles.schedule_runner_lambda + schedule_runner_scheduler = module.global.iam_roles.schedule_runner_scheduler } application_log_retention_days = local.environment.cloudwatch_log_groups.application_log_retention_days ecs_capacity_provider = local.ecs_capacity_provider @@ -102,6 +104,8 @@ module "eu_west_2" { fault_injection_simulator = module.global.iam_roles.fault_injection_simulator create_s3_batch_replication_jobs_lambda = module.global.iam_roles.create_s3_batch_replication_jobs_lambda event_received_lambda = module.global.iam_roles.event_received_lambda + schedule_runner_lambda = module.global.iam_roles.schedule_runner_lambda + schedule_runner_scheduler = module.global.iam_roles.schedule_runner_scheduler } application_log_retention_days = local.environment.cloudwatch_log_groups.application_log_retention_days ecs_capacity_provider = local.ecs_capacity_provider