diff --git a/app/config.py b/app/config.py deleted file mode 100644 index 9fe85c2..0000000 --- a/app/config.py +++ /dev/null @@ -1,16 +0,0 @@ -from configparser import ConfigParser - -def config(filename='database.ini', section='postgresql'): - parser = ConfigParser() - parser.read(filename) - - db = {} - if parser.has_section(section): - params = parser.items(section) - for param in params: - db[param[0]] = param[1] - - else: - raise Exception(f'Section {section} not found in the {filename}') - - return db \ No newline at end of file diff --git a/app/database.ini b/app/database.ini deleted file mode 100644 index 5a07986..0000000 --- a/app/database.ini +++ /dev/null @@ -1,5 +0,0 @@ -[postgresql] -host=localhost -database=suppliers -user=admin -password=admin \ No newline at end of file diff --git a/app/deployment/dev.tfvars b/app/deployment/dev.tfvars new file mode 100644 index 0000000..a4b79b1 --- /dev/null +++ b/app/deployment/dev.tfvars @@ -0,0 +1,5 @@ +env = "dev" +k8s_secret_name = "gcr-json-key" +k8s_secret_password = "key.json" +k8s_secret_server = "gcr.io" +k8s_secret_username = "_json_key" \ No newline at end of file diff --git a/app/deployment/helm/Chart.yaml b/app/deployment/helm/Chart.yaml new file mode 100644 index 0000000..c0cc75c --- /dev/null +++ b/app/deployment/helm/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: flask-api-chart +description: A Helm chart for deploying Flash application to GKE +version: 1.0.0 +appVersion: 0.0.1 \ No newline at end of file diff --git a/app/deployment/helm/templates/deployment.yaml b/app/deployment/helm/templates/deployment.yaml new file mode 100644 index 0000000..0b0edaa --- /dev/null +++ b/app/deployment/helm/templates/deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.app.name }} + namespace: {{ .Values.app.namespace }} + labels: + app: {{ .Values.app.name }} +spec: + replicas: {{ .Values.app.replicas }} + selector: + matchLabels: + app: {{ .Values.app.name }} + template: + metadata: + labels: + app: {{ .Values.app.name }} + spec: + imagePullSecrets: + - name: {{ .Values.imagePullSecrets.name }} + containers: + - name: {{ .Values.app.name }} + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: POSTGRES_USER + value: "{{ .Values.database.user }}" + - name: POSTGRES_PASSWORD + value: "{{ .Values.database.password }}" + - name: POSTGRES_HOST + value: "{{ .Values.database.host }}" + - name: POSTGRES_DB + value: "{{ .Values.database.dbname }}" + - name: POSTGRES_PORT + value: "{{ .Values.database.port }}" + resources: + limits: + cpu: "{{ .Values.app.resources.cpu }}" + memory: "{{ .Values.app.resources.memory }}" diff --git a/app/deployment/helm/templates/hpa.yaml b/app/deployment/helm/templates/hpa.yaml new file mode 100644 index 0000000..f1b2162 --- /dev/null +++ b/app/deployment/helm/templates/hpa.yaml @@ -0,0 +1,21 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Values.hpa.name }} + namespace: {{ .Values.hpa.namespace }} + labels: + app: {{ .Values.app.name }} +spec: + scaleTargetRef: + kind: {{ .Values.hpa.targetKind }} + name: {{ .Values.hpa.targetName }} + apiVersion: {{ .Values.hpa.targetAPIVersion }} + minReplicas: {{ .Values.hpa.minReplicas }} + maxReplicas: {{ .Values.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: {{ .Values.hpa.metricName }} + target: + type: {{ .Values.hpa.metricType }} + averageValue: {{ .Values.hpa.metricAverageValue }} diff --git a/app/deployment/helm/templates/service.yaml b/app/deployment/helm/templates/service.yaml new file mode 100644 index 0000000..d491ee8 --- /dev/null +++ b/app/deployment/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.name }} + namespace: {{ .Values.service.namespace }} + labels: + app: {{ .Values.app.name }} +spec: + selector: + app: {{ .Values.app.name }} + type: {{ .Values.service.type }} + ports: + - name: tcp + protocol: TCP + port: {{ .Values.service.tcpPort }} + targetPort: {{ .Values.service.targetPort }} + - name: http + port: {{ .Values.service.httpPort }} + targetPort: {{ .Values.service.targetPort }} diff --git a/app/deployment/helm/values.yaml b/app/deployment/helm/values.yaml new file mode 100644 index 0000000..81d072c --- /dev/null +++ b/app/deployment/helm/values.yaml @@ -0,0 +1,42 @@ +app: + name: flask-api + namespace: default + replicas: 3 + resources: + cpu: "0.1" + memory: "64Mi" + +image: + repository: gcr.io/developing-stuff/flask-api + tag: v0.0.2 + pullPolicy: Always + +database: + user: admin + password: admin + host: 10.30.244.110 + port: "5432" + dbname: databesos + +imagePullSecrets: + name: gcr-json-key + +service: + name: flask-api-service + namespace: default + type: LoadBalancer + tcpPort: 25443 + httpPort: 80 + targetPort: 5000 + +hpa: + name: flask-api-hpa + namespace: default + targetKind: Deployment + targetName: flask-api + targetAPIVersion: apps/v1 + minReplicas: 1 + maxReplicas: 5 + metricName: cpu + metricType: AverageValue + metricAverageValue: 40m \ No newline at end of file diff --git a/app/deployment/helm/values/dev.yaml b/app/deployment/helm/values/dev.yaml new file mode 100644 index 0000000..4d0e65e --- /dev/null +++ b/app/deployment/helm/values/dev.yaml @@ -0,0 +1,42 @@ +app: + name: flask-api + namespace: default + replicas: 3 + resources: + cpu: "0.1" + memory: "64Mi" + +image: + repository: gcr.io/developing-stuff/flask-api + tag: v0.0.2 + pullPolicy: Always + +database: + user: admin + password: admin + host: bitnami-postgresql.default.svc.cluster.local + port: "5432" + dbname: databesos + +imagePullSecrets: + name: gcr-json-key + +service: + name: flask-api-service + namespace: default + type: LoadBalancer + tcpPort: 25443 + httpPort: 80 + targetPort: 5000 + +hpa: + name: flask-api-hpa + namespace: default + targetKind: Deployment + targetName: flask-api + targetAPIVersion: apps/v1 + minReplicas: 1 + maxReplicas: 5 + metricName: cpu + metricType: AverageValue + metricAverageValue: 40m \ No newline at end of file diff --git a/app/deployment/helm/values/prod.yaml b/app/deployment/helm/values/prod.yaml new file mode 100644 index 0000000..4d0e65e --- /dev/null +++ b/app/deployment/helm/values/prod.yaml @@ -0,0 +1,42 @@ +app: + name: flask-api + namespace: default + replicas: 3 + resources: + cpu: "0.1" + memory: "64Mi" + +image: + repository: gcr.io/developing-stuff/flask-api + tag: v0.0.2 + pullPolicy: Always + +database: + user: admin + password: admin + host: bitnami-postgresql.default.svc.cluster.local + port: "5432" + dbname: databesos + +imagePullSecrets: + name: gcr-json-key + +service: + name: flask-api-service + namespace: default + type: LoadBalancer + tcpPort: 25443 + httpPort: 80 + targetPort: 5000 + +hpa: + name: flask-api-hpa + namespace: default + targetKind: Deployment + targetName: flask-api + targetAPIVersion: apps/v1 + minReplicas: 1 + maxReplicas: 5 + metricName: cpu + metricType: AverageValue + metricAverageValue: 40m \ No newline at end of file diff --git a/app/deployment/main.tf b/app/deployment/main.tf new file mode 100644 index 0000000..dd22f5b --- /dev/null +++ b/app/deployment/main.tf @@ -0,0 +1,12 @@ +module "flask-app" { + source = "../modules/flask-app" + env = var.env +} + +module "gke_secret" { + source = "../modules/gke-secret" + k8s_secret_name = var.k8s_secret_name + k8s_secret_password = var.k8s_secret_password + k8s_secret_server = var.k8s_secret_server + k8s_secret_username = var.k8s_secret_username +} \ No newline at end of file diff --git a/app/deployment/prod.tfvars b/app/deployment/prod.tfvars new file mode 100644 index 0000000..55ed77f --- /dev/null +++ b/app/deployment/prod.tfvars @@ -0,0 +1,5 @@ +env = "prod" +k8s_secret_name = "gcr-json-key" +k8s_secret_password = "key.json" +k8s_secret_server = "gcr.io" +k8s_secret_username = "_json_key" \ No newline at end of file diff --git a/app/deployment/providers.tf b/app/deployment/providers.tf new file mode 100644 index 0000000..c5290f4 --- /dev/null +++ b/app/deployment/providers.tf @@ -0,0 +1,7 @@ +# https://registry.terraform.io/providers/hashicorp/helm/latest/docs +provider "helm" { + kubernetes { + config_path = var.kubeconfig_path + } + alias = "gke" +} \ No newline at end of file diff --git a/app/deployment/variables.tf b/app/deployment/variables.tf new file mode 100644 index 0000000..597f0a3 --- /dev/null +++ b/app/deployment/variables.tf @@ -0,0 +1,29 @@ +variable "env" { + description = "Environment variable" + type = string + + validation { + condition = contains(["prod", "dev"], var.env) + error_message = "Error! \"${var.env}\" not in acceptable values: \"prod\", \"env\"" + } +} + +variable "k8s_secret_name" { + type = string + description = "Kubernetes secret name" +} + +variable "k8s_secret_server" { + type = string + description = "Kubernetes server name" +} + +variable "k8s_secret_username" { + type = string + description = "Kubernetes username" +} + +variable "k8s_secret_password" { + type = string + description = "Kubernetes password" +} \ No newline at end of file diff --git a/app/deployment/version.tf b/app/deployment/version.tf new file mode 100644 index 0000000..6f615a4 --- /dev/null +++ b/app/deployment/version.tf @@ -0,0 +1,14 @@ +# https://www.terraform.io/docs/language/settings/index.html +terraform { + required_version = ">= 1.0.0" + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 2.3.0" + } + } + backend "gcs" { + bucket = "tf-flask-app-v1" + prefix = "app_state" + } +} \ No newline at end of file diff --git a/app/functions.py b/app/functions.py deleted file mode 100644 index bd2b28b..0000000 --- a/app/functions.py +++ /dev/null @@ -1,12 +0,0 @@ -import re - -def dateVerification(date=str): - pattern = "^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$" - try: - re.match(pattern, date) - return True - except: - return False - - -dateVerification(1) \ No newline at end of file diff --git a/app/main.py b/app/main.py deleted file mode 100644 index c1f2967..0000000 --- a/app/main.py +++ /dev/null @@ -1,64 +0,0 @@ -from flask import Flask, request, jsonify -from flask_sqlalchemy import SQLAlchemy -from db import PostgreSQL -import re, os - - -# db_user = os.environ["DB_USER"] -# db_pass = os.environ["DB_PASS"] -# db_host = os.environ["DB_HOST"] -# db_name = os.environ["DB_NAME"] - -db_user, db_pass, db_host, db_name, db_port= "admin", "admin", "localhost", "suppliers", "5432" - -app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql://{db_user}:{db_pass}@{db_host}/{db_name}' -db = SQLAlchemy(app) - -db_conn = PostgreSQL(db_name, db_user, db_pass, db_host, db_port) -db_conn.connect() - -def verifyDate(date=str): - pattern = "(?:19\d{2}|20[01][0-9]|2023)[-/.](?:0[1-9]|1[012])[-/.](?:0[1-9]|[12][0-9]|3[01])\b" - try: - re.match(pattern, date) - return True - except: - return False - -@app.route('/hello/', methods=['GET','PUT']) -def hello_world(username): - if request.method == 'PUT': - if request.is_json: - json_data = request.get_json() - if 'dateOfBirth' in json_data: - dob = json_data['dateOfBirth'] - if verifyDate(dob): - try: - db_conn.createUpdateUser(username, dob) - return "OK", 204 - except: - return 400 - else: - return jsonify({"message":"dateOfBirth format isn't YYYY-MM-DD", "status": 400}), 400 - else: - return jsonify({"message":"Missing required attribute \"dateOfBirth\"", "status": 400}), 400 - else: - return jsonify({"message":f"Request not in json format {type(request)}", "status": 400}), 400 - else: - try: - if db_conn.isBirthday(username) == 0: - return { - "message":f"Hello, {username.capitalize()}! Happy birthday!" - }, 200 - else: - return { - "message":f"Hello, {username.capitalize()}! Your birthday is in {db_conn.isBirthday(username)} day(s)!" - }, 200 - except IndexError as e: - return { - "message":f"Error occurred {e}" - }, 400 - -if __name__ == "__main__": - app.run(debug='False') diff --git a/app/modules/flask-app/main.tf b/app/modules/flask-app/main.tf new file mode 100644 index 0000000..d1a27cc --- /dev/null +++ b/app/modules/flask-app/main.tf @@ -0,0 +1,9 @@ +resource "helm_release" "flask-api" { + name = "flask-api-helm" + repository = "file://./helm" + chart = "./helm" + + values = [ + "${file("./helm/values/${var.env}.yaml")}" + ] +} \ No newline at end of file diff --git a/app/modules/flask-app/variables.tf b/app/modules/flask-app/variables.tf new file mode 100644 index 0000000..6d02ae6 --- /dev/null +++ b/app/modules/flask-app/variables.tf @@ -0,0 +1,9 @@ +variable "env" { + description = "Environment variable" + type = string + + validation { + condition = contains(["prod", "dev"], var.env) + error_message = "Error! \"${var.env}\" not in acceptable values: \"prod\", \"env\"" + } +} \ No newline at end of file diff --git a/app/modules/gke-secret/main.tf b/app/modules/gke-secret/main.tf new file mode 100644 index 0000000..69451b4 --- /dev/null +++ b/app/modules/gke-secret/main.tf @@ -0,0 +1,17 @@ +resource "kubernetes_secret" "gcr-json-key" { + metadata { + name = var.k8s_secret_name + } + + data = { + ".dockerconfigjson" = jsonencode({ + auths = { + "${var.k8s_secret_server}" = { + auth = base64encode("${var.k8s_secret_username}:${file("${path.module}/${var.k8s_secret_password}")}") + } + } + }) + } + + type = "kubernetes.io/dockerconfigjson" +} \ No newline at end of file diff --git a/app/modules/gke-secret/variables.tf b/app/modules/gke-secret/variables.tf new file mode 100644 index 0000000..d95e941 --- /dev/null +++ b/app/modules/gke-secret/variables.tf @@ -0,0 +1,19 @@ +variable "k8s_secret_name" { + type = string + description = "Kubernetes secret name" +} + +variable "k8s_secret_server" { + type = string + description = "Kubernetes server name" +} + +variable "k8s_secret_username" { + type = string + description = "Kubernetes username" +} + +variable "k8s_secret_password" { + type = string + description = "Kubernetes password" +} \ No newline at end of file diff --git a/app/src/Dockerfile b/app/src/Dockerfile new file mode 100644 index 0000000..5a95c1e --- /dev/null +++ b/app/src/Dockerfile @@ -0,0 +1,23 @@ +# Use an official Python runtime as a parent image +FROM python:3.7-alpine + +# Set the working directory to /app +WORKDIR /app + +# Copy the requirements file into the container +COPY requirements.txt . + +# Install any needed packages specified in requirements.txt +RUN pip install -r requirements.txt + +# Copy the rest of the application code into the container +COPY . . + +# Set the environment variable for Flask +ENV FLASK_APP=main.py + +# Expose port 5000 for the Flask app to listen on +EXPOSE 5000 + +# Run the command to start the Flask app +CMD ["flask", "run", "--host=0.0.0.0"] diff --git a/app/src/api/user.py b/app/src/api/user.py new file mode 100644 index 0000000..ad4ce9c --- /dev/null +++ b/app/src/api/user.py @@ -0,0 +1,12 @@ +from flask import Blueprint, request +from services.user_service import get_user_service, put_user_service + +user_route = Blueprint('user_route', __name__) + +@user_route.route('/hello/', methods=['GET']) +def hello_world_get(username): + return get_user_service(username) + +@user_route.route('/hello/', methods=['PUT']) +def hello_world_put(username): + return put_user_service(username, request) \ No newline at end of file diff --git a/app/src/config.py b/app/src/config.py new file mode 100644 index 0000000..3f51dc0 --- /dev/null +++ b/app/src/config.py @@ -0,0 +1,30 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +db_user = os.environ['POSTGRES_USER'] +db_pass = os.environ['POSTGRES_PASSWORD'] +db_host = os.environ['POSTGRES_HOST'] +db_name = os.environ['POSTGRES_DB'] +db_port = os.environ['POSTGRES_PORT'] + +class Config(object): + DEBUG = False + DEVELOPMENT = False + TESTING = False + CSRF_ENABLED = False + SECRET_KEY = "" + SQLALCHEMY_DATABASE_URI = f'postgresql://{db_user}:{db_pass}@{db_host}/{db_name}' + +class ProductionConfig(Config): + DEBUG = False + +class StagingConfig(Config): + DEBUG = True + DEVELOPMENT = True + +class DevelopmentConfig(Config): + DEVELOPMENT = True + DEBUG = True + +class TestingConfig(Config): + TESTING = True \ No newline at end of file diff --git a/app/src/docker-compose.yaml b/app/src/docker-compose.yaml new file mode 100644 index 0000000..b5cdf81 --- /dev/null +++ b/app/src/docker-compose.yaml @@ -0,0 +1,9 @@ +version: '3' +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "5000:5000" + \ No newline at end of file diff --git a/app/src/main.py b/app/src/main.py new file mode 100644 index 0000000..f7c7315 --- /dev/null +++ b/app/src/main.py @@ -0,0 +1,18 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_cors import CORS +from api.user import user_route +import os +from config import ProductionConfig + +config = ProductionConfig() + +app = Flask(__name__) +app.register_blueprint(user_route) + +CORS(app) +app.config.from_object(config) +db = SQLAlchemy(app) + +if __name__ == "__main__": + app.run(debug='False', host='0.0.0.0') diff --git a/app/src/models/user.py b/app/src/models/user.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/requirements.txt b/app/src/requirements.txt new file mode 100644 index 0000000..dc4c0b8 --- /dev/null +++ b/app/src/requirements.txt @@ -0,0 +1,5 @@ +psycopg2-binary +flask +flask_sqlalchemy +flask_cors +python-dotenv \ No newline at end of file diff --git a/app/src/services/user_service.py b/app/src/services/user_service.py new file mode 100644 index 0000000..dc89fc4 --- /dev/null +++ b/app/src/services/user_service.py @@ -0,0 +1,53 @@ +from flask import make_response +from utils.VerifyDate import VerifyDate +from utils.DbConnection import PostgreSQL +import os + +db_user = os.environ["POSTGRES_USER"] +db_pass = os.environ["POSTGRES_PASSWORD"] +db_host = os.environ["POSTGRES_HOST"] +db_name = os.environ["POSTGRES_DB"] +db_port = os.environ["POSTGRES_PORT"] + +db_conn = PostgreSQL( + dbname = db_name, + password = db_pass, + host = db_host, + user = db_user, + port = db_port + ) + +def put_user_service(username, request): + if request.is_json: + json_data = request.get_json() + if 'dateOfBirth' in json_data: + dob = json_data['dateOfBirth'] + if VerifyDate.verifyDate(dob): + db_conn.connect() + try: + db_conn.createUpdateUser(username, dob) + return make_response({}, 204) + except: + return make_response({}, 400) + finally: + db_conn.disconnect() + else: + return make_response({"message" : "dateOfBirth format isn't YYYY-MM-DD", "status": 400}, 400) + else: + return make_response({"message":"Missing required attribute \"dateOfBirth\"", "status": 400}, 400) + else: + return make_response({"message":f"Request not in json format {type(request)}", "status": 400}, 400) + +def get_user_service(username): + db_conn.connect() + try: + if db_conn.isBirthday(username) == 0: + return make_response({"message":f"Hello, {username.capitalize()}! Happy birthday!"}, 200) + if db_conn.isBirthday(username) != 0 and db_conn.isBirthday(username) is not None: + return make_response({"message":f"Hello, {username.capitalize()}! Your birthday is in {db_conn.isBirthday(username)} day(s)!"}, 200) + elif db_conn.isBirthday(username) == None: + return make_response({"message":f"Error: User {username.capitalize()} doesn't exist."}, 400) + except Exception as e: + return make_response({"message":f"Error occurred {e}"}, 400) + finally: + db_conn.disconnect() \ No newline at end of file diff --git a/app/db.py b/app/src/utils/DbConnection.py similarity index 56% rename from app/db.py rename to app/src/utils/DbConnection.py index 4d363f5..3de405e 100644 --- a/app/db.py +++ b/app/src/utils/DbConnection.py @@ -28,42 +28,46 @@ def connect(self): def createUpdateUser(self, username, dateOfBirth): try: with self.conn.cursor() as cur: - cur.execute("SELECT 1 FROM users WHERE username=%s", (username.lower(),)) + cur.execute(f"SELECT 1 FROM usuarios WHERE username='{username.lower()}'") user_exists = cur.fetchone() if user_exists: - cur.execute("UPDATE users SET dateOfBirth=%s WHERE username=%s", (dateOfBirth, username.lower())) + cur.execute(f"UPDATE usuarios SET dateOfBirth='{dateOfBirth}' WHERE username='{username.lower()}'") self.conn.commit() - return {"status": 204, "message": f"User {username.lower()} updated successfully."} + return {"status": 204, "message": f"User '{username.lower()}' updated successfully."}, 204 else: - cur.execute("INSERT INTO users (username, dateOfBirth) VALUES (%s, %s)", (username.lower(), dateOfBirth)) + cur.execute(f"INSERT INTO usuarios (username, dateOfBirth) VALUES ('{username.lower()}', '{dateOfBirth}')") self.conn.commit() - return {"status": 204, "message": f"User '{username.lower()}' created successfully."} + return {"status": 204, "message": f"User '{username.lower()}' created successfully."}, 204 except Error as e: print(f"Error creating user: {e}") self.conn.rollback() - return {"status": 400, "message": str(e)} + return {"status": 400, "message": str(e)}, 400 def isBirthday(self, username): date_format = '%Y-%m-%d' today = datetime.today() try: with self.conn.cursor() as cur: - cur.execute("SELECT dateOfBirth FROM users WHERE username=%s", (username.lower(),)) + cur.execute(f"SELECT dateOfBirth FROM usuarios WHERE username='{username.lower()}'") birthday = cur.fetchone() - birthday = datetime.strptime(birthday[0], date_format) - birthday = birthday.replace(year=today.year) - delta = abs(birthday - today) - if today > birthday: - return 365 - delta.days - if today == birthday: - return 0 + if birthday == None: + return None else: - return delta.days + birthday = datetime.strptime(str(birthday[0]), date_format) + birthday = birthday.replace(year=today.year) + delta = abs(birthday - today) + if today > birthday: + return 365 - delta.days + if today == birthday: + return 0 + else: + return delta.days except Error as e: print(f"Error fetching birthday: {e}") - + # except NoneType as e: + # print(f"Error, no user: {e}") def disconnect(self): self.conn.close() print("Disconnected from database") diff --git a/app/src/utils/VerifyDate.py b/app/src/utils/VerifyDate.py new file mode 100644 index 0000000..9bcce78 --- /dev/null +++ b/app/src/utils/VerifyDate.py @@ -0,0 +1,10 @@ +import re + +class VerifyDate: + def verifyDate(date): + pattern = "(?:19\d{2}|20[01][0-9]|2023)[-/.](?:0[1-9]|1[012])[-/.](?:0[1-9]|[12][0-9]|3[01])\b" + try: + re.match(pattern, date) + return True + except: + return False \ No newline at end of file diff --git a/atlantis/dev.tfvars b/atlantis/dev.tfvars new file mode 100644 index 0000000..f6ab921 --- /dev/null +++ b/atlantis/dev.tfvars @@ -0,0 +1,5 @@ +namespace_name = "atlantis" +env = "dev" +cluster_name = "flask-app-v1-dev" +project_id = "developing-stuff" +region = "europe-west1" \ No newline at end of file diff --git a/atlantis/main.tf b/atlantis/main.tf new file mode 100644 index 0000000..9876b0e --- /dev/null +++ b/atlantis/main.tf @@ -0,0 +1,22 @@ +locals { + namespace_name = "${var.namespace_name}-${var.env}" +} + +resource "kubernetes_namespace" "example" { + metadata { + name = local.namespace_name + annotations = { + name = local.namespace_name + } + } +} + +# resource "helm_release" "atlantis" { +# name = "atlantis-${var.env}" +# repository = "file://./helm" +# chart = "./helm" +# namespace = "atlantis-${var.env}" +# values = [ +# "${file("./helm/values/${var.env}.yaml")}" +# ] +# } \ No newline at end of file diff --git a/atlantis/providers.tf b/atlantis/providers.tf new file mode 100644 index 0000000..9c75a53 --- /dev/null +++ b/atlantis/providers.tf @@ -0,0 +1,3 @@ +provider "kubernetes" { + config_path = "~/.kube/config" +} \ No newline at end of file diff --git a/atlantis/variables.tf b/atlantis/variables.tf new file mode 100644 index 0000000..8687504 --- /dev/null +++ b/atlantis/variables.tf @@ -0,0 +1,30 @@ +variable "project_id" { + type = string + description = "Project ID" +} + +variable "region" { + type = string + description = "Region name" +} + +variable "cluster_name" { + type = string + description = "Cluster name" +} + +variable "namespace_name" { + type = string + description = "Namespace name to be created" +} + +variable "env" { + type = string + description = "Environment: \"prod\" or \"dev\"" + + validation { + condition = contains(["prod", "dev"], var.env) + error_message = "Error! \"${var.env}\" not in possible values: \"prod\" or \"dev\"" + } +} + diff --git a/atlantis/version.tf b/atlantis/version.tf new file mode 100644 index 0000000..01f4cf0 --- /dev/null +++ b/atlantis/version.tf @@ -0,0 +1,14 @@ +# https://www.terraform.io/docs/language/settings/index.html +terraform { + required_version = ">= 1.0.0" + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 2.3.0" + } + } + backend "gcs" { + bucket = "tf-flask-app-v1" + prefix = "atlantis_state" + } +} \ No newline at end of file diff --git a/gcp/gke/dev.tfvars b/gcp/gke/dev.tfvars new file mode 100644 index 0000000..204851a --- /dev/null +++ b/gcp/gke/dev.tfvars @@ -0,0 +1,32 @@ +project_id = "developing-stuff" + +region = "europe-west1" + +gke_num_nodes = { + max = 10 + min = 1 +} + +gke_preemptible = true + +gke_machine_type = "n1-standard-1" + +gke_subnet_ip_cidr_range = "10.51.0.0/20" + +gke_cluster_autoscaling = { + enabled = true + cpu_minimum = 1 + cpu_maximum = 4 + memory_minimum = 4 + memory_maximum = 16 +} + +gke_disabled_hpa = true + +env_name = "dev" + +cluster_name = "flask-app-v1" + +ip_range_pods_name = "ip-range-pods-name" + +ip_range_services_name = "ip-range-services-name" \ No newline at end of file diff --git a/gcp/gke/main.tf b/gcp/gke/main.tf new file mode 100644 index 0000000..3a3c7e0 --- /dev/null +++ b/gcp/gke/main.tf @@ -0,0 +1,89 @@ +# Create a new private K8s cluster +# Source: https://github.com/terraform-google-modules/terraform-google-kubernetes-engine/tree/master/modules/private-cluster +module "gke" { + source = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster" + version = "= 23.1.0" + name = "${var.cluster_name}-${var.env_name}" + project_id = var.project_id + region = var.region + network = module.gke_vpc.network_name + # subnetwork = module.gke_vpc.subnets["subnet_name"] + subnetwork = "${var.subnetwork}-${var.env_name}" + ip_range_pods = var.ip_range_pods_name + ip_range_services = var.ip_range_services_name + horizontal_pod_autoscaling = var.gke_disabled_hpa + enable_vertical_pod_autoscaling = var.gke_disabled_vpa + cluster_autoscaling = { + enabled = var.gke_cluster_autoscaling.enabled + min_cpu_cores = var.gke_cluster_autoscaling.cpu_minimum + max_cpu_cores = var.gke_cluster_autoscaling.cpu_maximum + min_memory_gb = var.gke_cluster_autoscaling.memory_minimum + max_memory_gb = var.gke_cluster_autoscaling.memory_maximum + gpu_resources = [] + } + + node_pools = [ + { + name = "${var.cluster_name}-${var.env_name}-node-pool" + machine_type = var.gke_machine_type + node_locations = "${var.region}-c" + min_count = var.gke_num_nodes.min + max_count = var.gke_num_nodes.max + preemptible = var.gke_preemptible + initial_node_count = 1 + remove_default_node_pool = true + } + ] + + depends_on = [ + module.gke_vpc + ] +} + +# Create a VPC +# Source: https://github.com/terraform-google-modules/terraform-google-network +module "gke_vpc" { + source = "terraform-google-modules/network/google" + version = "~> 4.0" + project_id = var.project_id + network_name = "${var.network}-${var.env_name}" + # routing_mode = "GLOBAL" + subnets = [ + { + subnet_name = "${var.subnetwork}-${var.env_name}" + subnet_ip = var.gke_subnet_ip_cidr_range + subnet_region = var.region + } + ] + secondary_ranges = { + "${var.subnetwork}-${var.env_name}" = [ + { + ip_cidr_range = "10.20.0.0/16" + range_name = var.ip_range_pods_name + }, + { + ip_cidr_range = "10.30.0.0/16" + range_name = var.ip_range_services_name + } + ] + } +} + +# GKE Auth for retrieving token +# Source: https://github.com/terraform-google-modules/terraform-google-kubernetes-engine/tree/master/modules/auth +module "gke_auth" { + source = "terraform-google-modules/kubernetes-engine/google//modules/auth" + project_id = var.project_id + cluster_name = module.gke.name + location = module.gke.location + # use_private_endpoint = true + depends_on = [ + module.gke + ] +} + +# Save kubeconfig file +resource "local_file" "kubeconfig" { + content = module.gke_auth.kubeconfig_raw + filename = "./kubeconfig-${var.env_name}" +} \ No newline at end of file diff --git a/gcp/gke/prod.tfvars b/gcp/gke/prod.tfvars new file mode 100644 index 0000000..b8682c1 --- /dev/null +++ b/gcp/gke/prod.tfvars @@ -0,0 +1,32 @@ +project_id = "developing-stuff" + +region = "europe-west1" + +gke_num_nodes = { + max = 10 + min = 1 +} + +gke_preemptible = true + +gke_machine_type = "n1-standard-1" + +gke_subnet_ip_cidr_range = "10.51.0.0/20" + +gke_cluster_autoscaling = { + enabled = true + cpu_minimum = 1 + cpu_maximum = 4 + memory_minimum = 4 + memory_maximum = 16 +} + +gke_disabled_hpa = false + +env_name = "prod" + +cluster_name = "flask-app-v1" + +ip_range_pods_name = "ip-range-pods-name" + +ip_range_services_name = "ip-range-services-name" \ No newline at end of file diff --git a/gcp/gke/providers.tf b/gcp/gke/providers.tf new file mode 100644 index 0000000..6afd575 --- /dev/null +++ b/gcp/gke/providers.tf @@ -0,0 +1,4 @@ +provider "google" { + project = var.project_id + region = var.region +} \ No newline at end of file diff --git a/gcp/gke/variables.tf b/gcp/gke/variables.tf new file mode 100644 index 0000000..2b75464 --- /dev/null +++ b/gcp/gke/variables.tf @@ -0,0 +1,110 @@ +variable "project_id" { + default = "" + description = "GCP project id" + type = string +} + +variable "region" { + default = "" + description = "GCP region" + type = string +} + +variable "gke_num_nodes" { + default = { + min = 1 + max = 2 + } + description = "Number of GKE nodes" + type = object({ + min = number + max = number + }) +} + +variable "gke_preemptible" { + default = true + description = "Boolean variable for setting preemtible machines or not on GKE cluster" + type = bool +} + +variable "gke_machine_type" { + default = "" + description = "GKE machine type" + type = string +} + +variable "gke_disabled_hpa" { + default = true + description = "Boolean. Is HPA disabled?" + type = bool +} + +variable "gke_disabled_vpa" { + default = true + description = "Boolean. Is VPA disabled?" + type = bool +} + +variable "gke_cluster_autoscaling" { + default = { + enabled = true + cpu_minimum = 2 + cpu_maximum = 4 + memory_minimum = 4 + memory_maximum = 16 + } + description = "Object with information for enabling GKE cluster autoscaling" + type = object({ + enabled = bool + cpu_minimum = number + cpu_maximum = number + memory_minimum = number + memory_maximum = number + }) +} + +variable "gke_subnet_ip_cidr_range" { + default = "" + description = "ip cidr range for gcp subnet" + type = string +} + +variable "cluster_name" { + description = "The name for the GKE cluster" + type = string +} + +variable "env_name" { + description = "The environment for the GKE cluster" + default = "dev" + type = string +} + +variable "network" { + description = "The VPC network created to host the cluster in" + default = "gke-network" +} + +variable "subnetwork" { + description = "The subnetwork created to host the cluster in" + default = "gke-subnet" + type = string +} + +variable "ip_range_pods_name" { + description = "Name of the IP range pods name" + default = "ip_range_pods_name" + type = string +} + +variable "ip_range_services_name" { + description = "Name of the IP services pods name" + default = "ip_range_services_name" + type = string +} + +variable "namespaces" { + type = map(object) + description = "List of namespaces" +} \ No newline at end of file diff --git a/gcp/gke/version.tf b/gcp/gke/version.tf new file mode 100644 index 0000000..958f863 --- /dev/null +++ b/gcp/gke/version.tf @@ -0,0 +1,13 @@ +terraform { + # required_version = ">= 1.3.1" + required_providers { + google = { + source = "hashicorp/google" + version = "4.54.0" + } + } + backend "gcs" { + bucket = "tf-flask-app-v1" + prefix = "state" + } +} \ No newline at end of file diff --git a/postgresql/deployment/dev.tfvars b/postgresql/deployment/dev.tfvars new file mode 100644 index 0000000..a4aa92a --- /dev/null +++ b/postgresql/deployment/dev.tfvars @@ -0,0 +1,13 @@ +env = "dev" + +cluster_name = "flask-app-v1-dev" + +location = "europe-west1" + +kubeconfig_path = "~/.kube/config" + +pgDb = "pgdb" + +pgPass = "admin" + +pgUser = "admin" \ No newline at end of file diff --git a/postgresql/deployment/main.tf b/postgresql/deployment/main.tf new file mode 100644 index 0000000..6afb6fd --- /dev/null +++ b/postgresql/deployment/main.tf @@ -0,0 +1,8 @@ +# Deploy pgsql cluster on GKE +module "postgresql" { + source = "../modules" + env = var.env + pgPass = var.pgPass + pgUser = var.pgUser + pgDb = var.pgDb +} \ No newline at end of file diff --git a/postgresql/deployment/prod.tfvars b/postgresql/deployment/prod.tfvars new file mode 100644 index 0000000..3bf759c --- /dev/null +++ b/postgresql/deployment/prod.tfvars @@ -0,0 +1,5 @@ +env = "prod" + +cluster_name = "flask-app-v1-prod" + +location = "europe-west1" \ No newline at end of file diff --git a/postgresql/deployment/providers.tf b/postgresql/deployment/providers.tf new file mode 100644 index 0000000..c5290f4 --- /dev/null +++ b/postgresql/deployment/providers.tf @@ -0,0 +1,7 @@ +# https://registry.terraform.io/providers/hashicorp/helm/latest/docs +provider "helm" { + kubernetes { + config_path = var.kubeconfig_path + } + alias = "gke" +} \ No newline at end of file diff --git a/postgresql/deployment/variables.tf b/postgresql/deployment/variables.tf new file mode 100644 index 0000000..bde600b --- /dev/null +++ b/postgresql/deployment/variables.tf @@ -0,0 +1,38 @@ +variable "pgPass" { + description = "PostgreSQL Password" + type = string +} + +variable "pgUser" { + description = "PostgreSQL Username" + type = string +} + +variable "pgDb" { + description = "PostgreSQL Database" + type = string +} +variable "kubeconfig_path" { + description = "GKE kubeconfig path (usually ~/.kube/config)" + type = string +} + +variable "cluster_name" { + type = string + description = "GKE cluster name" +} + +variable "location" { + type = string + description = "GKE Location" +} + +variable "env" { + description = "Environment" + type = string + + validation { + condition = contains(["prod", "dev"], var.env) + error_message = "${var.env} not in accepted values: \"prod\", \"env\"" + } +} diff --git a/postgresql/deployment/version.tf b/postgresql/deployment/version.tf new file mode 100644 index 0000000..369a6d9 --- /dev/null +++ b/postgresql/deployment/version.tf @@ -0,0 +1,14 @@ +# https://www.terraform.io/docs/language/settings/index.html +terraform { + required_version = ">= 1.0.0" + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 2.3.0" + } + } + backend "gcs" { + bucket = "tf-flask-app-v1" + prefix = "postgresql_state" + } +} \ No newline at end of file diff --git a/postgresql/init.sql b/postgresql/init.sql new file mode 100644 index 0000000..9a0871b --- /dev/null +++ b/postgresql/init.sql @@ -0,0 +1,9 @@ +CREATE DATABASE databesos; + +\c databesos + +CREATE TABLE usuarios ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) NOT NULL, + dateOfBirth DATE NOT NULL +); \ No newline at end of file diff --git a/postgresql/modules/main.tf b/postgresql/modules/main.tf new file mode 100644 index 0000000..f1c1631 --- /dev/null +++ b/postgresql/modules/main.tf @@ -0,0 +1,54 @@ +# PGSQL DEV +# Chart documentation: https://artifacthub.io/packages/helm/bitnami/postgresql +resource "helm_release" "postgresql_dev" { + count = var.env == "prod" ? 0 : 1 + name = "bitnami" + repository = "https://charts.bitnami.com/bitnami" + chart = "postgresql" + + values = [ + "${file("${path.module}/values.yaml")}" + ] + + set_sensitive { + name = "global.postgresql.auth.postgresPassword" + value = var.pgPass + } + set_sensitive { + name = "global.postgresql.auth.username" + value = var.pgUser + } + set_sensitive { + name = "global.postgresql.auth.password" + value = var.pgPass + } + set_sensitive { + name = "global.postgresql.auth.database" + value = var.pgDb + } +} + +// PGSQL PROD +resource "helm_release" "postgresql_prod" { + count = var.env == "prod" ? 1 : 0 + name = "bitnami" + repository = "https://charts.bitnami.com/bitnami" + chart = "postgresql-ha" + + set_sensitive { + name = "global.postgresql.auth.postgresPassword" + value = var.pgPass + } + set_sensitive { + name = "global.postgresql.auth.username" + value = var.pgUser + } + set_sensitive { + name = "global.postgresql.auth.password" + value = var.pgPass + } + set_sensitive { + name = "global.postgresql.auth.database" + value = var.pgDb + } +} \ No newline at end of file diff --git a/postgresql/modules/variables.tf b/postgresql/modules/variables.tf new file mode 100644 index 0000000..cbe08f7 --- /dev/null +++ b/postgresql/modules/variables.tf @@ -0,0 +1,24 @@ +variable "env" { + description = "Environment" + type = string + + validation { + condition = contains(["prod", "dev"], var.env) + error_message = "${var.env} not in accepted values: \"prod\", \"env\"" + } +} + +variable "pgPass" { + description = "PostgreSQL Password" + type = string +} + +variable "pgUser" { + description = "PostgreSQL Username" + type = string +} + +variable "pgDb" { + description = "PostgreSQL Database" + type = string +} \ No newline at end of file