Table of Contents
- Technical Task
- Implementation Details
- Project Setup
- Up & Running Locally
- Tests
- the Run with IntelliJ/GoLand IDE or Run with Docker Compose.
- The campsite can be reserved for a maximum of 3 days.
- The campsite can be reserved a minimum of 1 day(s) ahead of arrival and up to 1 month in advance.
- Reservations can be canceled anytime.
- For the sake of simplicity, assume the check-in & check-out time is 12:00 AM.
- The users will need to find out when the campsite is available. So, the system should expose an API to provide information on the availability of the campsite for a given date range, with the default being 1 month.
- Provide an endpoint for reserving the campsite. The user will provide his/her email & full name at the time of reserving the campsite along with the intended arrival date and departure date. Return a unique booking identifier to the caller if the reservation succeeds.
- The unique booking identifier can be used to modify or cancel the reservation later on. Provide appropriate endpoint (s) to allow modification/cancellation of an existing reservation.
- Due to the popularity of the campsite, there is a high likelihood of multiple users attempting to reserve the campsite for the same/overlapping date(s). Demonstrate with appropriate test cases that the system can gracefully handle concurrent requests to reserve the campsite.
- Provide appropriate error messages to the caller to indicate the error cases.
- The system should be able to handle a large volume of requests to determine campsite availability.
- There are no restrictions on how reservations are stored as long as system constraints are not violated.
Technologies used:
- Go, gRPC
- protovalidate-go (requests validation)
- goimports, golines, gofumpt (code style & formatting)
- Goose (DB migrations)
- PostgreSQL
- Docker, Docker Compose
- TODO
TODO: elaborate on implementation details
Prerequisites:
- Git, see this guide on how to install Git.
- Make
- Go (version >= 1.22), see this guide on how to install Go.
Clone the project and install the necessary tools(protoc, mockery, golines, goimports, gofumpt, golangci-lint):
$ git clone https://github.com/igor-baiborodine/campsite-booking-go.git
$ cd campsite-booking-go
$ make install-tools
If you use either IntelliJ IDEA or GoLand IDEs, follow this guide to configure it.
⚠️ Please note that all commands listed below should be executed from the project's root.
- Go to Run | Edit Configurations... and create a new
Run/Debug
configuration for the Campgrounds API as follows:
- Start a PostgreSQL DB instance using Docker Compose:
$ docker compose -f docker/docker-compose.yml -p campsite-booking-go up -d postgres
- Verify the health status of the running
postgres
container:
$ docker inspect --format="{{.State.Health.Status}}" postgres
- If the output is
healthy
, launch theRun/Debug
configuration created in the previous step.
- Start PostgreSQL DB and Campgrounds API instances using Docker Compose:
$ docker compose -f docker/docker-compose.yml -p campsite-booking-go up -d
Prerequisites:
- Install kind:
$ go install sigs.k8s.io/kind@$latest
$ kind version
- Install kubectl:
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256"
$ echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
$ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
$ kubectl version --client
- Spin up a 3-node cluster:
$ make cluster-deploy
# which is equivalent of
$ kind create cluster --name local-k8s --config ./k8s/kind-config.yaml
$ kubectl cluster-info --context kind-local-k8s
- Deploy PostgreSQL DB, Campgrounds API, and Envoy proxy:
$ make all-deploy
# which is equivalent of
# db-deploy
$ kubectl create secret generic postgres-secret --from-literal=POSTGRES_PASSWORD=postgres
$ kubectl create secret generic campgrounds-secret --from-literal=CAMPGROUNDS_PASSWORD=campgrounds_pass
$ kubectl create configmap initdb-config --from-file=./db/init/
$ kubectl apply -f ./k8s/postgres.yaml
# api-deploy
$ kubectl apply -f ./k8s/campgrounds.yaml
# proxy-deploy:
$ kubectl create configmap envoy-config --from-file=./k8s/envoy-config.yaml
$ kubectl apply -f ./k8s/envoy.yaml
- Verify the status of created pods:
$ kubectl get pods
# which may look like this
NAME READY STATUS RESTARTS AGE
campgrounds-796fff564f-dgfsm 1/1 Running 2 (81s ago) 89s
campgrounds-796fff564f-qj44x 1/1 Running 2 (81s ago) 89s
campgrounds-796fff564f-vqfjz 1/1 Running 3 (61s ago) 89s
envoy-9dbcd5c66-h4p9v 1/1 Running 0 89s
postgres-0 1/1 Running 0 2m13s
- Use the
port-forward
command to forward Envoy’s port8080
tolocalhost:8080
to test the Campgrounds services:
$ PROXY_POD_NAME=$(kubectl get pods --selector=app=envoy -o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward "$PROXY_POD_NAME" 8080:8080
- Execute only unit tests:
$ make test
# which is equivalent of
$ go test -race ./internal/...
- Execute only integration tests:
$ make test-integration
# which is equivalent of
$ go test -tags=integration ./internal/...
Prerequisites:
- Install gRPCurl:
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
$ grpcurl -version
- The Campgrounds API should be up & running using either the Run with IntelliJ/GoLand IDE or Run with Docker Compose.
- List the services present on the gRPC server:
$ grpcurl -plaintext localhost:8085 list
# output
campgroundspb.v1.CampgroundsService
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
- List all the RPC endpoints the
campgroundspb.v1.CampgroundsService
contains:
$ grpcurl -plaintext localhost:8085 describe campgroundspb.v1.CampgroundsService
# output
campgroundspb.v1.CampgroundsService is a service:
service CampgroundsService {
rpc CancelBooking ( .campgroundspb.v1.CancelBookingRequest ) returns ( .campgroundspb.v1.CancelBookingResponse );
rpc CreateBooking ( .campgroundspb.v1.CreateBookingRequest ) returns ( .campgroundspb.v1.CreateBookingResponse );
rpc CreateCampsite ( .campgroundspb.v1.CreateCampsiteRequest ) returns ( .campgroundspb.v1.CreateCampsiteResponse );
rpc GetBooking ( .campgroundspb.v1.GetBookingRequest ) returns ( .campgroundspb.v1.GetBookingResponse );
rpc GetCampsites ( .campgroundspb.v1.GetCampsitesRequest ) returns ( .campgroundspb.v1.GetCampsitesResponse );
rpc GetVacantDates ( .campgroundspb.v1.GetVacantDatesRequest ) returns ( .campgroundspb.v1.GetVacantDatesResponse );
rpc UpdateBooking ( .campgroundspb.v1.UpdateBookingRequest ) returns ( .campgroundspb.v1.UpdateBookingResponse );
}
- Get a gRPC message definition, for example for
campgroundspb.v1.GetBookingRequest
:
grpcurl -plaintext localhost:8085 describe campgroundspb.v1.GetBookingRequest
# output
campgroundspb.v1.GetBookingRequest is a message:
message GetBookingRequest {
string booking_id = 1 [(.buf.validate.field) = { string: { uuid: true } }];
}
- Create a campsite:
$ grpcurl -plaintext -d \
'{"campsite_code": "CAMP01", "capacity": 4, "drinking_water": true, "fire_pit": true, "picnic_table": true, "restrooms": false}' \
localhost:8085 campgroundspb.v1.CampgroundsService/CreateCampsite
# output
{
"campsiteId": "07df7f35-9c7a-4b10-a702-66844a7ec08c"
}
- Get campsites:
$ grpcurl -plaintext -d '{}' localhost:8085 campgroundspb.v1.CampgroundsService/GetCampsites
# output
{
"campsites": [
{
"campsiteId": "07df7f35-9c7a-4b10-a702-66844a7ec08c",
"campsiteCode": "CAMP01",
"capacity": 4,
"drinkingWater": true,
"picnicTable": true,
"firePit": true,
"active": true
}
]
}
- Create a booking:
$ grpcurl -plaintext -d \
'{"campsite_id": "07df7f35-9c7a-4b10-a702-66844a7ec08c", "email": "john.smith@example.com", "full_name": "John Smith", "start_date": "2024-09-09", "end_date": "2024-09-12"}' \
localhost:8085 campgroundspb.v1.CampgroundsService/CreateBooking
# output
{
"bookingId": "692abbc0-5457-4f2b-8a6e-061ba2e5dd90"
}
- Get a booking:
$ grpcurl -plaintext -d \
'{"booking_id": "692abbc0-5457-4f2b-8a6e-061ba2e5dd90"}' \
localhost:8085 campgroundspb.v1.CampgroundsService/GetBooking
# output
{
"booking": {
"bookingId": "692abbc0-5457-4f2b-8a6e-061ba2e5dd90",
"campsiteId": "07df7f35-9c7a-4b10-a702-66844a7ec08c",
"email": "john.smith@example.com",
"fullName": "John Smith",
"startDate": "2024-09-09",
"endDate": "2024-09-12",
"active": true
}
}
- Create a booking that does not meet the booking constraints, for example a maximum stay of three days:
$ grpcurl -plaintext -d \
'{"campsite_id": "07df7f35-9c7a-4b10-a702-66844a7ec08c", "email": "john.smith@example.com", "full_name": "John Smith", "start_date": "2024-09-15", "end_date": "2024-09-20"}' \
localhost:8085 campgroundspb.v1.CampgroundsService/CreateBooking
# output
ERROR:
Code: InvalidArgument
Message: booking validation: 1 error occurred:
* maximum stay: must be less or equal to three days