Skip to content

Commit

Permalink
PFS-143 upload file to s3 (#15)
Browse files Browse the repository at this point in the history
* PFS-143 Create back-end structure similar to finance-hub

* PFS-143 Create back-end structure similar to finance-hub

* PFS-143 Add test upload file code

* PFS-143 Update makefile and build yml

* PFS-143 Fix linting

* PFS-143 Add localstack for local bucket

* PFS-143 Upload file from form

* PFS-143 Fix linting

* PFS-143 Add SSE to upload

* PFS-143 Add success message, remove test upload type

* PFS-143 Migrate to aws-sdk-go-v2

* PFS-143 Attempt to fix blank endpoint

* PFS-143 Attempt to fix blank endpoint

* PFS-143 Undo previous change

* PFS-143 Move uploads to subfolder in bucket

* PFS-143 Move CSV header validation to back-end

* PFS-143 Add cypress test & api unit tests

* PFS-143 Create ADR

* PFS-143 Fix build

* PFS-143 Code cleanup, move upload type enum to shared

* PFS-143 Change case of upload type enum, undo SSE env var

* PFS-143 Fix cypress test

* PFS-143 Fix cypress test
  • Loading branch information
josephsmith0705 authored Sep 26, 2024
1 parent 4b79699 commit f09a7c1
Show file tree
Hide file tree
Showing 92 changed files with 1,453 additions and 516 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,24 @@ jobs:
with:
registries: 311462405659

- name: Generate Single Timestamp
run: echo "TIMESTAMP=$(date +"%Y%m%d%H%M%S")" >> $GITHUB_ENV

- name: Push Container
run: |
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:${{ steps.bump_version.outputs.tag }}
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:${{ steps.bump_version.outputs.tag }}
if [ $BRANCH_NAME == "main" ]; then
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:main-${{ steps.bump_version.outputs.tag }}
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:main-${{ steps.bump_version.outputs.tag }}-$(date +"%Y%m%d%H%M%S")
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:main-${{ steps.bump_version.outputs.tag }}
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:main-${{ steps.bump_version.outputs.tag }}-$TIMESTAMP
docker tag 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:latest 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:main-${{ steps.bump_version.outputs.tag }}-$TIMESTAMP
# We want all of the tags pushed
docker push --all-tags 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin
docker push --all-tags 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api
else
docker push 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin:${{ steps.bump_version.outputs.tag }}
docker push 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:${{ steps.bump_version.outputs.tag }}
fi
push-tags:
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ go-lint:
docker compose run --rm go-lint

build:
docker compose build --no-cache --parallel finance-admin
docker compose build --no-cache --parallel finance-admin finance-admin-api

build-dev:
docker compose -f docker-compose.yml -f docker/docker-compose.dev.yml build --parallel finance-admin yarn
docker compose -f docker-compose.yml -f docker/docker-compose.dev.yml build --parallel finance-admin finance-admin-api yarn

build-all:
docker compose build --parallel finance-admin yarn cypress
docker compose build --parallel finance-admin finance-admin-api yarn cypress

test: setup-directories
go run gotest.tools/gotestsum@latest --format testname --junitfile test-results/unit-tests.xml -- ./... -coverprofile=test-results/test-coverage.txt
Expand All @@ -31,7 +31,7 @@ clean:
docker compose run --rm yarn

up: clean build-dev
docker compose -f docker-compose.yml -f docker/docker-compose.dev.yml up finance-admin yarn
docker compose -f docker-compose.yml -f docker/docker-compose.dev.yml up finance-admin finance-admin-api yarn

down:
docker compose down
Expand Down
File renamed without changes.
23 changes: 23 additions & 0 deletions adrs/0003-new-finance-back-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 3. Create a new Finance Admin Back End

Date: 2024-09-24

## Status

Accepted

## Context

We have created the Finance Admin front end, however the decision was made to add a service between S3 and the front end
to upload and download files from S3 without making AWS credentials accessible to the user.

## Decision

A new back end will be created in this repo, similarly to the structure of the finance hub. This will communicate with S3
to perform uploads and downloads using Go's AWS SDK. This back end will also allow us to perform any other logic required for
the finance admin system, such as validation.

## Consequences

This ensures we can interact with S3 whilst keeping our AWS credentials away from the client, but increases the complexity of the system.

79 changes: 79 additions & 0 deletions apierror/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package apierror

import (
"fmt"
"net/http"
"strings"
)

type BadRequest struct {
error
Field string `json:"field,omitempty"`
Reason string `json:"reason"`
}

func BadRequestError(field string, reason string, error error) *BadRequest {
return &BadRequest{error: error, Field: field, Reason: reason}
}

func (b BadRequest) Error() string {
return b.Reason
}

func (b BadRequest) HTTPStatus() int { return http.StatusBadRequest }

func (b BadRequest) HasData() bool {
return true
}

type BadRequests struct {
Reasons []string `json:"reasons"`
}

func BadRequestsError(reasons []string) *BadRequests {
return &BadRequests{Reasons: reasons}
}

func (b BadRequests) Error() string {
return fmt.Sprintf("bad requests: %s", strings.Join(b.Reasons, ", "))
}

func (b BadRequests) HTTPStatus() int { return http.StatusBadRequest }

func (b BadRequests) HasData() bool {
return true
}

type NotFound struct {
error
}

func NotFoundError(error error) *NotFound {
return &NotFound{error: error}
}

func (n NotFound) Unwrap() error {
return n.error
}

func (n NotFound) Error() string {
return "requested resource not found"
}

func (n NotFound) HTTPStatus() int { return http.StatusNotFound }

type ValidationErrors map[string]map[string]string

type ValidationError struct {
Errors ValidationErrors `json:"validation_errors"`
}

func (ve ValidationError) Error() string {
return "validation failed"
}

func (ve ValidationError) HTTPStatus() int { return http.StatusUnprocessableEntity }

func (ve ValidationError) HasData() bool {
return true
}
29 changes: 29 additions & 0 deletions cypress/cypress.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { defineConfig } = require("cypress")

module.exports = defineConfig({
fixturesFolder: false,
e2e: {
setupNodeEvents(on, config) {
on("task", {
log(message) {
console.log(message);

return null
},
table(message) {
console.table(message);

return null
},
failed: require("cypress-failed-log/src/failed")()
});
},
baseUrl: "http://localhost:8888/finance-admin",
specPattern: "e2e/**/*.cy.{js,ts}",
screenshotsFolder: "screenshots",
supportFile: "support/e2e.ts",
modifyObstructiveCode: false,
},
viewportWidth: 1000,
viewportHeight: 1000,
});
41 changes: 41 additions & 0 deletions cypress/e2e/upload.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe("Finance Admin", () => {
beforeEach(() => {
cy.visit("/uploads");
});

describe("Upload file", () => {
it("Uploads file successfully", () => {
cy.get('[data-cy=\"report-upload-type\"]').select('DEBT_CHASE')
cy.get('#file-upload').selectFile('cypress/fixtures/debt_chase_example.csv')
cy.get('.govuk-button').contains('Upload report').click()
cy.url().should("include","/uploads?success=upload");
cy.get('.moj-banner').contains('File successfully uploaded')
});

it("Validates missing file", () => {
cy.get('[data-cy=\"report-upload-type\"]').select('DEPUTY_SCHEDULE')
cy.get('.govuk-button').contains('Upload report').click()
cy.get('.govuk-error-summary').contains('No file uploaded')
cy.get('#f-FileUpload').contains('No file uploaded')
cy.get('#f-FileUpload').should('have.class', 'govuk-form-group--error')
})

it("Validates empty headers", () => {
cy.get('[data-cy=\"report-upload-type\"]').select('DEPUTY_SCHEDULE')
cy.get('#file-upload').selectFile('cypress/fixtures/empty_report.csv')
cy.get('.govuk-button').contains('Upload report').click()
cy.get('.govuk-error-summary').contains('Failed to read CSV headers')
cy.get('#f-FileUpload').contains('Failed to read CSV headers')
cy.get('#f-FileUpload').should('have.class', 'govuk-form-group--error')
})

it("Validates CSV headers", () => {
cy.get('[data-cy=\"report-upload-type\"]').select('DEPUTY_SCHEDULE');
cy.get('#file-upload').selectFile('cypress/fixtures/debt_chase_example.csv')
cy.get('.govuk-button').contains('Upload report').click()
cy.get('.govuk-error-summary').contains('CSV headers do not match for the report trying to be uploaded')
cy.get('#f-FileUpload').contains('CSV headers do not match for the report trying to be uploaded')
cy.get('#f-FileUpload').should('have.class', 'govuk-form-group--error')
});
});
});
1 change: 1 addition & 0 deletions cypress/fixtures/debt_chase_example.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Client_no,Deputy_name,Total_debt
Empty file.
46 changes: 44 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,53 @@ services:
environment:
PORT: 8888
PREFIX: /finance-admin
BACKEND_URL: http://finance-admin-api:8080
healthcheck:
test: wget -O /dev/null -S 'http://localhost:8888/finance-admin/health-check' 2>&1 | grep 'HTTP/1.1 200 OK' || exit 1
interval: 15s
timeout: 10s
retries: 3
start_period: 30s
depends_on:
finance-admin-api:
condition: service_healthy


finance-admin-api:
image: 311462405659.dkr.ecr.eu-west-1.amazonaws.com/sirius/sirius-finance-admin-api:latest
build:
dockerfile: docker/finance-admin-api/Dockerfile
ports:
- "8181:8080"
environment:
ASYNC_S3_BUCKET: opg-backoffice-async-uploads-local
AWS_REGION: eu-west-1
AWS_S3_ENDPOINT: http://localstack:4566
AWS_ACCESS_KEY_ID: localstack
AWS_SECRET_ACCESS_KEY: localstack
healthcheck:
test: wget -O /dev/null -S 'http://localhost:8080/health-check' 2>&1 | grep 'HTTP/1.1 200 OK' || exit 1
interval: 15s
timeout: 10s
retries: 3
start_period: 30s
depends_on:
localstack:
condition: service_healthy

localstack:
image: localstack/localstack:3.0
volumes:
- "./scripts/localstack/init:/etc/localstack/init/ready.d"
- "./scripts/localstack/wait:/scripts/wait"
environment:
AWS_DEFAULT_REGION: eu-west-1
healthcheck:
test: bash /scripts/wait/healthcheck.sh
interval: 20s
timeout: 30s
retries: 50
restart: unless-stopped

cypress:
build:
Expand Down Expand Up @@ -43,8 +84,9 @@ services:
- ./.trivyignore:/.trivyignore

yarn:
image: node:20-alpine3.19
image: node:22-alpine3.19
working_dir: /home/node/app
entrypoint: yarn
volumes:
- ./:/home/node/app
- ./finance-admin:/home/node/app

28 changes: 22 additions & 6 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@ services:
ports:
- '2345:2345'
volumes:
- ./.air.toml:/app/.air.toml
- ./go.mod:/app/go.mod
- ./go.sum:/app/go.sum
- ./internal:/app/internal
- ./main_test.go:/app/main_test.go
- ./main.go:/app/main.go
- ./web/static:/app/web/static
- ./web/template:/app/web/template
- ./shared:/app/shared
- ./finance-admin/.air.toml:/app/finance-admin/.air.toml
- ./finance-admin/internal:/app/finance-admin/internal
- ./finance-admin/main_test.go:/app/finance-admin/main_test.go
- ./finance-admin/main.go:/app/finance-admin/main.go
- ./finance-admin/web/static:/app/finance-admin/web/static
- ./finance-admin/web/template:/app/finance-admin/web/template

finance-admin-api:
build:
target: dev
ports:
- '3456:2345'
volumes:
- ./go.mod:/app/go.mod
- ./go.sum:/app/go.sum
- ./apierror:/app/apierror
- ./shared:/app/shared
- ./finance-admin-api/.air.toml:/app/finance-admin-api/.air.toml
- ./finance-admin-api/api:/app/finance-admin-api/api
- ./finance-admin-api/awsclient:/app/finance-admin-api/awsclient
- ./finance-admin-api/main.go:/app/finance-admin-api/main.go

yarn:
command: watch
40 changes: 40 additions & 0 deletions docker/finance-admin-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
FROM golang:1.22-alpine AS base
WORKDIR /app

ENV CGO_ENABLED=0 GOOS=linux

RUN update-ca-certificates

FROM base AS dev
WORKDIR /app/finance-admin-api

RUN go install github.com/cosmtrek/[email protected] && go install github.com/go-delve/delve/cmd/dlv@latest
EXPOSE 8080
EXPOSE 2345

ENTRYPOINT ["air"]

FROM base as build-env
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

WORKDIR /app/finance-admin-api

RUN go build -a -installsuffix cgo -o /go/bin/finance-admin-api

FROM alpine:3
WORKDIR /go/bin

RUN apk --update --no-cache add \
ca-certificates \
tzdata

# Patch vulnerabilities
RUN apk upgrade --no-cache busybox libcrypto3 libssl3

COPY --from=build-env /go/bin/finance-admin-api finance-admin-api
ENTRYPOINT ["./finance-admin-api"]
Loading

0 comments on commit f09a7c1

Please sign in to comment.