diff --git a/.gitignore b/.gitignore index 15201ac..18cbeac 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,48 @@ cython_debug/ # PyPI configuration file .pypirc + +# Local .terraform directories +terraform/.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 +terraform/override.tf +terraform/override.tf.json +terraform/*_override.tf +terraform/*_override.tf.json + +# Ignore transient lock info files created by terraform apply +terraform/.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 +terraform/.terraformrc +terraform/terraform.rc + +# Ignore credentials file (any json file that starts with "vmassign-dev-") +terraform/vmassign-dev-*.json +terraform/service-account-*.json + +# ignore DS_Store files +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ede1787 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use a lightweight Python base image +FROM python:3.9-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements file +COPY app/requirements.txt requirements.txt + +# Install Python dependencies +RUN pip install -r requirements.txt + +# Copy the app code into the container +COPY ./app . + +# Expose the port Flask will run on +EXPOSE 8080 + +# Command to run the Flask app +CMD ["python", "app.py"] diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..c240475 --- /dev/null +++ b/app/app.py @@ -0,0 +1,17 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def home(): + return "Hello, World! This is a test Flask app." + + +@app.route("/health") +def health(): + return {"status": "healthy"}, 200 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..0f4e7db --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1 @@ +flask==2.2.5 diff --git a/create-service-account.sh b/create-service-account.sh new file mode 100644 index 0000000..320b846 --- /dev/null +++ b/create-service-account.sh @@ -0,0 +1,38 @@ +# Project ID as an argument +PROJECT_ID=$1 + +if [ -z "$PROJECT_ID" ]; then + echo "Project ID is required." + exit 1 +fi + +# Create the service account +gcloud iam service-accounts create service-account-admin \ + --description="Service account to create and manage other service accounts" \ + --display-name="Service Account Admin" \ + --project="${PROJECT_ID}" + +# Grant roles to allow service account management +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:service-account-admin@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/iam.serviceAccountAdmin" + +# Grant roles to allow assignment of roles to other service accounts +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:service-account-admin@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/iam.roleAdmin" + +# Grant database admin roles +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:service-account-admin@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/spanner.admin" + +# Grant cloud run deployment roles +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:service-account-admin@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/run.developer" + +# Generate a key for the service account +gcloud iam service-accounts keys create terraform/service-account-admin-key.json \ + --iam-account="service-account-admin@${PROJECT_ID}.iam.gserviceaccount.com" \ + --project="${PROJECT_ID}" \ No newline at end of file diff --git a/delete-service-account.sh b/delete-service-account.sh new file mode 100644 index 0000000..c1acbc5 --- /dev/null +++ b/delete-service-account.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Exit on any error +set -e + +# Variables +PROJECT_ID="vmassign-dev" +SERVICE_ACCOUNT_NAME="service-account-admin" +SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +echo "Deleting IAM bindings for service account: $SERVICE_ACCOUNT_EMAIL" + +gcloud projects get-iam-policy $PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:$SERVICE_ACCOUNT_EMAIL" + +# Get the IAM roles assigned to the service account +IAM_ROLES=$(gcloud projects get-iam-policy $PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:$SERVICE_ACCOUNT_EMAIL") + +echo "IAM roles assigned to $SERVICE_ACCOUNT_EMAIL: $IAM_ROLES" + +# Remove ROLE column header from the output +IAM_ROLES=$(echo "$IAM_ROLES" | sed '1d') + + +# Revoke IAM roles assigned to the service account +if [ -z "$IAM_ROLES" ]; then + echo "No active IAM roles found for $SERVICE_ACCOUNT_EMAIL." +else + # Remove IAM roles only for the active service account + for ROLE in $IAM_ROLES; do + echo "Removing IAM role: $ROLE" + gcloud projects remove-iam-policy-binding $PROJECT_ID \ + --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \ + --role="$ROLE" + done +fi + + +# Delete the service account +echo "Deleting service account: $SERVICE_ACCOUNT_EMAIL" +gcloud iam service-accounts delete $SERVICE_ACCOUNT_EMAIL --quiet + + +# Check if the service account still exists before deleting +if gcloud iam service-accounts describe $SERVICE_ACCOUNT_EMAIL &>/dev/null; then + echo "Deleting service account: $SERVICE_ACCOUNT_EMAIL" + gcloud projects set-iam-policy $PROJECT_ID <(gcloud projects get-iam-policy $PROJECT_ID --format=json | jq 'del(.bindings[] | select(.members[] | contains("serviceAccount:'"$SERVICE_ACCOUNT_EMAIL"'")))') +else + echo "Service account $SERVICE_ACCOUNT_EMAIL does not exist." +fi + +# Delete the key file +echo "Deleting key file: terraform/service-account-admin-key.json" +rm -f terraform/service-account-admin-key.json + +echo "Service account $SERVICE_ACCOUNT_EMAIL deleted successfully!" diff --git a/kubectl.sha256 b/kubectl.sha256 new file mode 100644 index 0000000..ad4b76a --- /dev/null +++ b/kubectl.sha256 @@ -0,0 +1 @@ +5b89f9598e2e7da04cc0b5dd6e8daca01d23855fd00c8ea259fd2aab993114db \ No newline at end of file diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..4b39e37 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,41 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.5.0" + constraints = "6.5.0" + hashes = [ + "h1:qKHzN9B+9uOjHDXxanQJanBdsd0bzCP3egUjIcyYxSY=", + "zh:14101a38e880d4a1ef14c0a24476b008a3b577540a260a246a471bcfb5c31f4c", + "zh:478a7b0810956d39843e785262ab8162324a7412c3f6cf1ceb43977e2c05f22e", + "zh:6c9b583abcbaa2093b1b55494ac08851bd3364919fe86850a9c3e8f6c46851d4", + "zh:7c400eb5488221ba7ea48725ab43db1464cefd96cb29a24e63fe1950666b465f", + "zh:82931b2c186403753356a73878d36efc209c9e5ae46d0b609bb7ca38aece931d", + "zh:87e7966ef7067de3684f658251cdede057be419bbfeaaad935ab6f501024046a", + "zh:a2f4aaa3b9260732a53f78c8053eb2cbcee2abf11d3d245c58f3065423ad30ab", + "zh:bbc4c3ca9d51287e77130fc95880792007dd919b9b5396433f9eed737119c6c3", + "zh:edcda54d37be1b8d4cbe029e30df6a228e0be3887831b892c11536502d87e840", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f95d92ca2ac527442f6767a217b8a557ba6b2d1915c39efba412382e986e4c3e", + "zh:f96148c3742126219b810a687551284460a8d78aa66efbfd2c58880f48049dda", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.3" + hashes = [ + "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=", + "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2", + "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d", + "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3", + "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f", + "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301", + "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670", + "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed", + "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65", + "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd", + "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5", + ] +} diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..7afa391 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,54 @@ +# Terraform in Lablink + +## Overview +This directory contains the Terraform configuration files for the Lablink project. The Terraform configuration files are used to create the infrastructure for the Lablink project. The infrastructure creates the following resources: + +- A virtual machine (VM) instance in Google Cloud Platform (GCP) with SLEAP installed. +- A database instance in Google Spanner for VM assignment. +- A Cloud Run service for the Lablink API. + +## Prerequisites + +Before you can use the Terraform configuration files, running Terraform commands, you need to install Terraform. You can install Terraform by following the instructions in the [Terraform documentation](https://learn.hashicorp.com/tutorials/terraform/install-cli). + +Also, you need to have a Google Cloud Platform (GCP) account and a project in GCP. You can create a GCP account and a project in the [Google Cloud Platform Official Website](https://cloud.google.com/gcp). + +After creating a GCP account and a project, you need to create a service account in GCP and download the service account key. You can create a service account by following the instructions in the [GCP Service Accounts documentation](https://cloud.google.com/iam/docs/creating-managing-service-accounts). After creating the service account, you need to download the service account key in JSON format. You can download the service account key by following the instructions in the [GCP Keys documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). + +## Configuration + +The Terraform configuration files are located in the `terraform` directory. The configuration files are organized as follows: + +- `main.tf`: The main Terraform configuration file that defines the resources to create. +- `variables.tf`: The Terraform variables file that defines the input variables for the Terraform configuration files. +- `outputs.tf`: The Terraform outputs file that defines the output variables for the Terraform configuration files. +- `provider.tf`: The Terraform provider file that defines the provider for the Terraform configuration files. + +## Installation + +To install the Terraform configuration files, clone the repository: + +```bash +git clone https://github.com/talmolab/lablink.git +``` + +## Usage + +To use the Terraform configuration files, you need to run this command to initialize the Terraform configuration files: + +```bash +cd terraform +terraform init +``` + +After initializing the Terraform configuration files, you can run the following command to create the infrastructure: + +```bash +terraform apply +``` + +To destroy the infrastructure, you can run the following command: + +```bash +terraform destroy +``` diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..12ae281 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,91 @@ +locals { + service_name = "${var.resource_suffix}-cosyne-service" + database_name = "users-${var.resource_suffix}" + is_test = var.resource_suffix == "test" + is_staging = var.resource_suffix == "staging" + is_production = var.resource_suffix == "prod" +} + +# Service Account Creation for spanner instance +resource "google_service_account" "spanner_admin_account" { + account_id = "spanner-admin" + display_name = "Spanner Admin Service Account" +} + +# Instance Creation +resource "google_spanner_instance" "database_instance" { + name = "vmassign-${var.resource_suffix}" + display_name = "Assign Instance ${var.resource_suffix}" + config = "regional-us-west1" + processing_units = 1000 +} + + +# Database Creation +resource "google_spanner_database" "default" { + name = "users" + instance = google_spanner_instance.database_instance.name + ddl = [ + "CREATE TABLE Users (Hostname STRING(1024) NOT NULL, Pin STRING(1024), CrdCmd STRING(1024), UserEmail STRING(1024), inUse BOOL,) PRIMARY KEY (Hostname)" + ] + deletion_protection = false +} + +# Grant permissions to the new service account for Spanner +resource "google_spanner_database_iam_member" "spanner_permissions" { + project = var.project_id + instance = google_spanner_instance.database_instance.name + database = google_spanner_database.default.name + role = "roles/spanner.databaseAdmin" + member = "serviceAccount:${google_service_account.spanner_admin_account.email}" +} + +# Service Account Creation for spanner instance +resource "google_service_account" "cloud_run_admin" { + account_id = "cloud-run-admin" + display_name = "Cloud Run Admin Service Account" +} + +# Deploy the web app +resource "google_cloud_run_service" "flask_service" { + # count = local.is_production || local.is_production || local.is_test ? 1 : 0 + name = "flask-service" + location = "us-central1" + + template { + spec { + containers { + image = "us-central1-docker.pkg.dev/vmassign-dev/flask-app-repo/flask-app:latest" + + resources { + limits = { + memory = "256Mi" + cpu = "1" + } + } + } + } + } + + traffic { + percent = 100 + latest_revision = true + } +} + +# Grant access to all users as an invoker +resource "google_cloud_run_service_iam_member" "invoker" { + # count = local.is_production ? 1 : 0 + service = google_cloud_run_service.flask_service.name + location = google_cloud_run_service.flask_service.location + role = "roles/run.invoker" + member = "allUsers" +} + +# Grant permissions to the new service account for Spanner +resource "google_cloud_run_service_iam_member" "cloud_run_permission" { + project = var.project_id + service = google_cloud_run_service.flask_service.name + role = "roles/run.developer" + member = "serviceAccount:${google_service_account.cloud_run_admin.email}" +} diff --git a/terraform/output.tf b/terraform/output.tf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/terraform/output.tf @@ -0,0 +1 @@ + diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..bbd09f6 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "6.5.0" + } + } +} + +provider "google" { + project = "vmassign-dev" + region = "us-west1" + credentials = "./service-account-admin-key.json" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..dd332a1 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,17 @@ +variable "instance_name" { + description = "Name of the instance" + type = string + default = "web-server" +} + +variable "project_id" { + description = "Target Project ID" + type = string + default = "vmassign-dev" +} + +variable "resource_suffix" { + description = "Suffix to append to all resources" + type = string + default = "prod" +}