diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..41d9723b Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml new file mode 100644 index 00000000..b2456554 --- /dev/null +++ b/.github/workflows/build-docker.yaml @@ -0,0 +1,49 @@ +name: Build and Publish Docker Images + +on: + push: + branches: + - main + paths: + - 'api/Dockerfile' + - 'front-end-nextjs/Dockerfile' + +jobs: + publish_images: + runs-on: ubuntu-latest + steps: + # Step 1: Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Step 2: Log in to Docker Hub + - name: Log in to Docker Hub + run: echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "echelonkay" --password-stdin + + # Step 3: Build API Docker image + - name: Build API Docker image + run: docker build ./api/ -t echelonkay/devops-qr-code-api:latest --no-cache + + # Step 4: Install dependencies for Frontend + - name: Install dependencies for Frontend + run: | + cd front-end-nextjs + npm install + + # Step 5: Build Frontend + - name: Build Frontend + run: | + cd front-end-nextjs + npm run build + + # Step 6: Build Frontend Docker image + - name: Build Frontend Docker image + run: docker build ./front-end-nextjs/ -t echelonkay/devops-qr-code-frontend:latest --no-cache + + # Step 7: Push API Docker image to Docker Hub + - name: Push API Docker image + run: docker push echelonkay/devops-qr-code-api:latest + + # Step 8: Push Frontend Docker image to Docker Hub + - name: Push Frontend Docker image + run: docker push echelonkay/devops-qr-code-frontend:latest diff --git a/README.md b/README.md index eb66952b..ecbcc5ba 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,40 @@ It generates QR Codes for the provided URL, the front-end is in NextJS and the A The API code exists in the `api` directory. You can run the API server locally: - Clone this repo +- Create a AWS User with AmazonS3FullAccess +- Create a S3 Bucket and select ACL enable for public access, Uncheck block bucket. - Make sure you are in the `api` directory -- Create a virtualenv by typing in the following command: `python -m venv .venv` -- Install the required packages: `pip install -r requirements.txt` +- Create a virtualenv by typing in the following command: `python3 -m venv .venv` +- Run command `source .venv/bin` +- Run command `source .venv/bin/activate` +<<<<<<< HEAD +- Install the required packages: `pip install -r requirements.txt` 'or pip install fastapi uvicorn boto3 python-dotenv pytest qrcode' +======= +- Install the required packages: `pip install -r requirements.txt` or `pip install fastapi uvicorn boto3 python-dotenv pytest qrcode` +>>>>>>> 9db5a81 (Moved Terraform files into the infrastructure folder) - Create a `.env` file, and add you AWS Access and Secret key, check `.env.example` +- and Save the Access & secret key, cat the `.env` to verify. - Also, change the BUCKET_NAME to your S3 bucket name in `main.py` - Run the API server: `uvicorn main:app --reload` - Your API Server should be running on port `http://localhost:8000` +### optional- Troubleshoting S3 bucket +### you might add Bucket policy + +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*" # Add bucket name + } + ] +} + + ### Front-end The front-end code exits in the `front-end-nextjs` directory. You can run the front-end server locally: diff --git a/api/* b/api/* new file mode 100644 index 00000000..90706812 --- /dev/null +++ b/api/* @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +import qrcode +import boto3 +import os +from io import BytesIO + +# Loading Environment variable (AWS Access Key and Secret Key) +from dotenv import load_dotenv +load_dotenv() + +app = FastAPI() + +# Allowing CORS for local testing +origins = [ + "http://localhost:3000" +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_methods=["*"], + allow_headers=["*"], +) + +# AWS S3 Configuration +s3 = boto3.client( + 's3', + aws_access_key_id= os.getenv("AWS_ACCESS_KEY"), + aws_secret_access_key= os.getenv("AWS_SECRET_KEY")) + +bucket_name = 'capstone-qr-code-bucket' # Add your bucket name here + +@app.post("/generate-qr/") +async def generate_qr(url: str): + # Generate QR Code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(url) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + # Save QR Code to BytesIO object + img_byte_arr = BytesIO() + img.save(img_byte_arr, format='PNG') + img_byte_arr.seek(0) + + # Generate file name for S3 + file_name = f"qr_codes/{url.split('//')[-1]}.png" + + try: + # Upload to S3 + s3.put_object(Bucket=bucket_name, Key=file_name, Body=img_byte_arr, ContentType='image/png', ACL='public-read') + + # Generate the S3 URL + s3_url = f"https://{bucket_name}.s3.amazonaws.com/{file_name}" + return {"qr_code_url": s3_url} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + \ No newline at end of file diff --git a/api/.DS_Store b/api/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/api/.DS_Store differ diff --git a/api/.env.example b/api/.env.example index 6083f141..295dffaa 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,2 +1,2 @@ -AWS_ACCESS_KEY=Your-AWS-Access-Key -AWS_SECRET_KEY=Your-AWS-Secret-Access-Key \ No newline at end of file +#AWS_ACCESS_KEY=Your-AWS-Access-Key +#AWS_SECRET_KEY=Your-AWS-Secret-Access-Key \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 00000000..52ee86aa --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,17 @@ +# Use the official image as a parent image +FROM python:3.9 + +# Set the working directory in the container +WORKDIR /urs/src/app + +# Copy the dependencies file to the working directory +COPY requirements.txt ./ + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the content of the local src directory to the working directory +COPY . . + +# Run the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file diff --git a/api/main.py b/api/main.py index d9d275b4..432fcff7 100644 --- a/api/main.py +++ b/api/main.py @@ -4,16 +4,16 @@ import boto3 import os from io import BytesIO - -# Loading Environment variable (AWS Access Key and Secret Key) from dotenv import load_dotenv + +# Load environment variables (AWS Access Key and Secret Key) load_dotenv() app = FastAPI() # Allowing CORS for local testing origins = [ - "http://localhost:3000" + "http://localhost:3000" # Update with your frontend URL if needed ] app.add_middleware( @@ -26,10 +26,11 @@ # AWS S3 Configuration s3 = boto3.client( 's3', - aws_access_key_id= os.getenv("AWS_ACCESS_KEY"), - aws_secret_access_key= os.getenv("AWS_SECRET_KEY")) + aws_access_key_id=os.getenv("AWS_ACCESS_KEY"), + aws_secret_access_key=os.getenv("AWS_SECRET_KEY") +) -bucket_name = 'YOUR_BUCKET_NAME' # Add your bucket name here +bucket_name = 'capstone-qr-code-bucket' # Replace with your S3 bucket name @app.post("/generate-qr/") async def generate_qr(url: str): @@ -54,12 +55,18 @@ async def generate_qr(url: str): file_name = f"qr_codes/{url.split('//')[-1]}.png" try: - # Upload to S3 - s3.put_object(Bucket=bucket_name, Key=file_name, Body=img_byte_arr, ContentType='image/png', ACL='public-read') + # Upload to S3 (removed ACL='public-read' as it caused the error) + s3.put_object( + Bucket=bucket_name, + Key=file_name, + Body=img_byte_arr, + ContentType='image/png' + ) # Generate the S3 URL s3_url = f"https://{bucket_name}.s3.amazonaws.com/{file_name}" return {"qr_code_url": s3_url} + except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - \ No newline at end of file + # Log error and raise HTTPException + raise HTTPException(status_code=500, detail=f"Error generating QR code: {str(e)}") diff --git a/api/test_main.py b/api/test_main.py index 78fc9741..8b45e484 100644 --- a/api/test_main.py +++ b/api/test_main.py @@ -4,14 +4,18 @@ client = TestClient(app) def test_generate_qr(): - url = "http://example.com" + url = "https://example.com" # Use a valid URL here response = client.post("/generate-qr/", json={"url": url}) - + + # Check that the response status code is 200 (OK) assert response.status_code == 200 + + # Check that the response contains the 'qr_code_url' field assert "qr_code_url" in response.json() def test_generate_qr_invalid_url(): - url = "invalid-url" + url = "invalid-url" # Invalid URL response = client.post("/generate-qr/", json={"url": url}) - assert response.status_code == 422 # FastAPI validation error \ No newline at end of file + # FastAPI validation should return status 422 for invalid input + assert response.status_code == 422 diff --git a/front-end-nextjs/Dockerfile b/front-end-nextjs/Dockerfile new file mode 100644 index 00000000..72739a75 --- /dev/null +++ b/front-end-nextjs/Dockerfile @@ -0,0 +1,32 @@ +# Use the official lightweight Node.js 18 image +FROM node:18-alpine AS base + +# Set the working directory in the container +WORKDIR /app + +# Copy the package files +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ + +# Copy the install-dependencies.sh script +COPY install-dependencies.sh ./ + +# Install dependencies using the script +RUN chmod +x ./install-dependencies.sh && ./install-dependencies.sh + +# Copy the rest of the application code +COPY . . + +# Run the Next.js build step +RUN npm run build + +# Expose the build folder +RUN mkdir -p .next + +# Copy the .next folder (to avoid context issues) +COPY .next ./.next + +# Expose the application port +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] diff --git a/front-end-nextjs/install-dependencies.sh b/front-end-nextjs/install-dependencies.sh new file mode 100755 index 00000000..81574300 --- /dev/null +++ b/front-end-nextjs/install-dependencies.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# Check and install dependencies based on the lockfile available +if [ -f yarn.lock ]; then + echo "Installing dependencies with Yarn..." + yarn install --frozen-lockfile +elif [ -f package-lock.json ]; then + echo "Installing dependencies with npm..." + npm ci +elif [ -f pnpm-lock.yaml ]; then + echo "Installing dependencies with pnpm..." + corepack enable pnpm + pnpm install --frozen-lockfile +else + echo "Error: Lockfile not found. Cannot determine package manager." + exit 1 +fi diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore new file mode 100644 index 00000000..21e6d3cb --- /dev/null +++ b/infrastructure/.gitignore @@ -0,0 +1,37 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc \ No newline at end of file diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl new file mode 100644 index 00000000..240bbd7b --- /dev/null +++ b/infrastructure/.terraform.lock.hcl @@ -0,0 +1,105 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.84.0" + constraints = "~> 5.0" + hashes = [ + "h1:OJ53RNte7HLHSMxSkzu1S6H8sC0T8qnCAOcNLjjtMpc=", + "zh:078f77438aba6ec8bf9154b7d223e5c71c48d805d6cd3bcf9db0cc1e82668ac3", + "zh:1f6591ff96be00501e71b792ed3a5a14b21ff03afec9a1c4a3fd9300e6e5b674", + "zh:2ab694e022e81dd74485351c5836148a842ed71cf640664c9d871cb517b09602", + "zh:33c8ccb6e3dc496e828a7572dd981366c6271075c1189f249b9b5236361d7eff", + "zh:6f31068ebad1d627e421c72ccdaafe678c53600ca73714e977bf45ff43ae5d17", + "zh:7488623dccfb639347cae66f9001d39cf06b92e8081975235a1ac3a0ac3f44aa", + "zh:7f042b78b9690a8725c95b91a70fc8e264011b836605bcc342ac297b9ea3937d", + "zh:88b56ac6c7209dc0a775b79975a371918f3aed8f015c37d5899f31deff37c61a", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a1979ba840d704af0932f8de5f541cbb4caa9b6bbd25ed552a24e6772175ba07", + "zh:b058c0533dae580e69d1adbc1f69e6a80632374abfc10e8634d06187a108e87b", + "zh:c88610af9cf957f8dcf4382e0c9ca566ef10e3290f5de01d4d90b2d81b078aa8", + "zh:e9562c055a2247d0c287772b55abef468c79f8d66a74780fe1c5e5dae1a284a9", + "zh:f7a7c71d28441d925a25c08c4485c015b2d9f0338bc9707443e91ff8e161d3d9", + "zh:fee533e81976d0900aa6fa443dc54ef171cbd901847f28a6e8edb1d161fa6fde", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.5" + constraints = ">= 2.0.0" + hashes = [ + "h1:Sf1Lt21oTADbzsnlU38ylpkl8YXP0Beznjcy5F/Yx64=", + "zh:17c20574de8eb925b0091c9b6a4d859e9d6e399cd890b44cfbc028f4f312ac7a", + "zh:348664d9a900f7baf7b091cf94d657e4c968b240d31d9e162086724e6afc19d5", + "zh:5a876a468ffabff0299f8348e719cb704daf81a4867f8c6892f3c3c4add2c755", + "zh:6ef97ee4c8c6a69a3d36746ba5c857cf4f4d78f32aa3d0e1ce68f2ece6a5dba5", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8283e5a785e3c518a440f6ac6e7cc4fc07fe266bf34974246f4e2ef05762feda", + "zh:a44eb5077950168b571b7eb65491246c00f45409110f0f172cc3a7605f19dba9", + "zh:aa0806cbff72b49c1b389c0b8e6904586e5259c08dabb7cb5040418568146530", + "zh:bec4613c3beaad9a7be7ca99cdb2852073f782355b272892e6ee97a22856aec1", + "zh:d7fe368577b6c8d1ae44c751ed42246754c10305c7f001cc0109833e95aa107d", + "zh:df2409fc6a364b1f0a0f8a9cd8a86e61e80307996979ce3790243c4ce88f2915", + "zh:ed3c263396ff1f4d29639cc43339b655235acf4d06296a7c120a80e4e0fd6409", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.35.1" + constraints = ">= 2.10.0" + hashes = [ + "h1:zgXeWvp4//Ry+4glwNrLMpPFOU8QBQlARNmR9WCNe9o=", + "zh:12212ca5ae47823ce14bfafb909eeb6861faf1e2435fb2fc4a8b334b3544b5f5", + "zh:3f49b3d77182df06b225ab266667de69681c2e75d296867eb2cf06a8f8db768c", + "zh:40832494d19f8a2b3cd0c18b80294d0b23ef6b82f6f6897b5fe00248a9997460", + "zh:739a5ddea61a77925ee7006a29c8717377a2e9d0a79a0bbd98738d92eec12c0d", + "zh:a02b472021753627c5c39447a56d125a32214c29ff9108fc499f2dcdf4f1cc4f", + "zh:b78865b3867065aa266d6758c9601a2756741478f5735a838c20d633d65e085b", + "zh:d362e87464683f5632790e66920ea803adb54c2bc0cb24b6fd9a314d2b1efffd", + "zh:d98206fe88c2c9a52b8d2d0cb2c877c812a4a51d19f9d8428e63cbd5fd8a304d", + "zh:dfa320946b1ce3f3615c42b3447a28dc9f604c06d8b9a6fe289855ab2ade4d11", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fc1debd2e695b5222d2ccc8b24dab65baba4ee2418ecce944e64d42e79474cb5", + "zh:fdaf960443720a238c09e519aeb30faf74f027ac5d1e0a309c3b326888e031d7", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.12.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=", + "zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2", + "zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea", + "zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511", + "zh:438b8c74f5ed62fe921bd1078abe628a6675e44912933100ea4fa26863e340e9", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:85c8bd8eefc4afc33445de2ee7fbf33a7807bc34eb3734b8eefa4e98e4cddf38", + "zh:98bbe309c9ff5b2352de6a047e0ec6c7e3764b4ed3dfd370839c4be2fbfff869", + "zh:9c7bf8c56da1b124e0e2f3210a1915e778bab2be924481af684695b52672891e", + "zh:d2200f7f6ab8ecb8373cda796b864ad4867f5c255cff9d3b032f666e4c78f625", + "zh:d8c7926feaddfdc08d5ebb41b03445166df8c125417b28d64712dccd9feef136", + "zh:e2412a192fc340c61b373d6c20c9d805d7d3dee6c720c34db23c2a8ff0abd71b", + "zh:e6ac6bba391afe728a099df344dbd6481425b06d61697522017b8f7a59957d44", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.6" + constraints = ">= 3.0.0" + hashes = [ + "h1:n3M50qfWfRSpQV9Pwcvuse03pEizqrmYEryxKky4so4=", + "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", + "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", + "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", + "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", + "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", + "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", + "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", + "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", + "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", + "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", + "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 00000000..42c9cfd3 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,85 @@ +# Create a VPC +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true +} + +# Create Subnet 1 +resource "aws_subnet" "subnet_1" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.0.0/20" + availability_zone = "eu-west-2a" + map_public_ip_on_launch = true +} + +# Create Subnet 2 +resource "aws_subnet" "subnet_2" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.16.0/20" # Updated CIDR to prevent overlap with Subnet 1 + availability_zone = "eu-west-2b" + map_public_ip_on_launch = true +} + +# Create Subnet 3 +resource "aws_subnet" "subnet_3" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.32.0/20" # Updated CIDR to prevent overlap with Subnet 1 + availability_zone = "eu-west-2c" + map_public_ip_on_launch = true +} + +# Create an Internet Gateway +resource "aws_internet_gateway" "internet_gw" { + vpc_id = aws_vpc.main.id +} + +# Create a Route Table +resource "aws_route_table" "route_table" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.internet_gw.id + } + + route { + cidr_block = "10.0.0.0/16" + gateway_id = "local" + } +} + +resource "aws_route_table_association" "subnet_1_association" { + subnet_id = aws_subnet.subnet_1.id + route_table_id = aws_route_table.route_table.id +} +resource "aws_route_table_association" "subnet_2_association" { + subnet_id = aws_subnet.subnet_2.id + route_table_id = aws_route_table.route_table.id +} +resource "aws_route_table_association" "subnet_3_association" { + subnet_id = aws_subnet.subnet_3.id + route_table_id = aws_route_table.route_table.id +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 19.0" + + cluster_name = "devops-capstone-project" + cluster_version = "1.27" + + cluster_endpoint_public_access = true + + vpc_id = aws_vpc.main.id + subnet_ids = [aws_subnet.subnet_1.id, aws_subnet.subnet_2.id, aws_subnet.subnet_3.id] + control_plane_subnet_ids = [aws_subnet.subnet_1.id, aws_subnet.subnet_2.id, aws_subnet.subnet_3.id] + + eks_managed_node_groups = { + green = { + min_size = 1 + max_size = 1 + desired_size = 1 + instance_types = ["t3.medium"] + } + } +} diff --git a/infrastructure/provider.tf b/infrastructure/provider.tf new file mode 100644 index 00000000..2ea2afba --- /dev/null +++ b/infrastructure/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +# Configure the AWS Provider +provider "aws" { + region = "eu-west-2" +}