diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..e422f2f --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,51 @@ +name: Continuous Integration and Continuous Deployment +run-name: ${{ github.actor }} submitted a CI CD Action +on: + push: + branches: [ "main", "milestone6", "milestone6-yp"] + +jobs: + Explore-GitHub-Actions: + if: contains(github.event.head_commit.message, '/run-') + runs-on: ubuntu-latest + steps: + - run: echo "Comment ${{ github.event.head_commit.message }}" + - run: echo "Job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "Job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "Branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - id: 'auth' + uses: google-github-actions/auth@v1 + with: + credentials_json: '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}' + - name: Configure Google Cloud SDK + uses: google-github-actions/setup-gcloud@v1 + - name: Configure Docker Client + run: |- + gcloud auth configure-docker # --quiet #authenticate to gcr + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - name: Build App Deploy Container + run: |- + cd ${{ github.workspace }}/src/app_deploy/ + docker build -t app_deployment --platform=linux/amd64 -f Dockerfile . + - name: Run Deploy App + if: contains(github.event.head_commit.message, '/run-deploy-app') + run: |- + docker run --rm --name app_deployment \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $HOME/.ssh:/home/app/.ssh \ + -v ${{ github.workspace }}/src/frontend:/frontend-react \ + -v ${{ github.workspace }}/src/api-service:/api-service \ + --volume $GITHUB_WORKSPACE:/workspace \ + --mount type=bind,source=$GOOGLE_APPLICATION_CREDENTIALS,target=/secrets/deployment.json \ + --env GOOGLE_APPLICATION_CREDENTIALS=/secrets/deployment.json \ + -e USE_GKE_GCLOUD_AUTH_PLUGIN=True \ + -e GCP_PROJECT=ac215project-398401 \ + -e GCP_ZONE=us-west3-b \ + app_deployment ./deploy-app.sh + - run: echo "Job's status is ${{ job.status }}." \ No newline at end of file diff --git a/src/api-service/api/model_backend.py b/src/api-service/api/model_backend.py index 9b00ac7..184e7c1 100644 --- a/src/api-service/api/model_backend.py +++ b/src/api-service/api/model_backend.py @@ -9,8 +9,6 @@ pwd = Path(__file__).parent.resolve() import sys, os sys.path.insert(0, "LLaVA") -# print(sys.path) -# print(os.environ.get("PYTHONPATH")) from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN from llava.conversation import conv_templates, SeparatorStyle from llava.model.builder import load_pretrained_model @@ -115,8 +113,15 @@ def post(self): app = Flask(__name__) swagger = Swagger(app) - app.add_url_rule('/chat', view_func=ChatView.as_view('chat'), methods=['POST']) + +@app.route('/status', methods=['GET']) +def get_api_status(): + return jsonify({ + "api_version": "1.0", + "torch_version": torch.__version__ + }) + CORS(app) # Run the Flask app diff --git a/src/app_deploy/Dockerfile b/src/app_deploy/Dockerfile index f0530a8..25a1116 100644 --- a/src/app_deploy/Dockerfile +++ b/src/app_deploy/Dockerfile @@ -3,6 +3,9 @@ FROM ubuntu:20.04 # Set the environment variable for non-interactive installations ENV DEBIAN_FRONTEND=noninteractive +ENV LANG=C.UTF-8 +ENV PYENV_SHELL=/bin/bash +ENV PYTHONUNBUFFERED=1 # Install required dependencies RUN apt-get update && \ @@ -32,7 +35,7 @@ RUN curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --d curl https://baltocdn.com/helm/signing.asc | apt-key add -&& \ echo "deb https://baltocdn.com/helm/stable/debian/ all main" | tee /etc/apt/sources.list.d/helm-stable-debian.list && \ apt-get update && \ - apt-get install -y --no-install-recommends kubectl helm python3 python3-pip && \ + apt-get install -y --no-install-recommends kubectl helm python3.9 python3-pip && \ pip install openshift ansible docker apache-libcloud RUN useradd -ms /bin/bash app -d /home/app -u 1000 -p "$(openssl passwd -1 passw0rd)" && \ diff --git a/src/app_deploy/deploy-app-init.sh b/src/app_deploy/deploy-app-init.sh new file mode 100644 index 0000000..7e1a4e6 --- /dev/null +++ b/src/app_deploy/deploy-app-init.sh @@ -0,0 +1,5 @@ +# Build and Push Docker Containers to GCR +ansible-playbook deploy-docker-images.yml -i inventory.yml +# Create and Deploy Cluster +ansible-playbook deploy-k8s-cluster.yml -i inventory.yml --extra-vars cluster_state=present +# Once the command runs go to http://.sslip.io \ No newline at end of file diff --git a/src/app_deploy/deploy-app.sh b/src/app_deploy/deploy-app.sh new file mode 100755 index 0000000..7cc51e2 --- /dev/null +++ b/src/app_deploy/deploy-app.sh @@ -0,0 +1,4 @@ +# Build and Push Docker Containers to GCR +ansible-playbook deploy-docker-images.yml -i inventory.yml +# Update Cluster +ansible-playbook update-k8s-cluster.yml -i inventory-prod.yml \ No newline at end of file diff --git a/src/app_deploy/docker-entrypoint.sh b/src/app_deploy/docker-entrypoint.sh index 9aae40b..1b11817 100644 --- a/src/app_deploy/docker-entrypoint.sh +++ b/src/app_deploy/docker-entrypoint.sh @@ -9,4 +9,12 @@ gcloud config set project $GCP_PROJECT # Configure GCR gcloud auth configure-docker gcr.io -q -/bin/bash \ No newline at end of file +args="$@" +echo $args + +if [[ -z ${args} ]]; +then + /bin/bash +else + /bin/bash $args +fi \ No newline at end of file diff --git a/src/app_deploy/docker-shell.sh b/src/app_deploy/docker-shell.sh index 8b9f68f..0d94342 100644 --- a/src/app_deploy/docker-shell.sh +++ b/src/app_deploy/docker-shell.sh @@ -1,7 +1,7 @@ #!/bin/bash # exit immediately if a command exits with a non-zero status -#set -e +set -e # Define some environment variables export IMAGE_NAME="app_deployment" diff --git a/src/app_deploy/inventory-prod.yml b/src/app_deploy/inventory-prod.yml new file mode 100644 index 0000000..66aa9d2 --- /dev/null +++ b/src/app_deploy/inventory-prod.yml @@ -0,0 +1,9 @@ +all: + vars: + gcp_service_account_file: "/secrets/deployment.json" + gcp_service_account_email: "deployment@ac215project-398401.iam.gserviceaccount.com" + gcp_auth_kind: "serviceaccount" + gcp_scopes: "https://www.googleapis.com/auth/compute" + gcp_project: "ac215project-398401" + gcp_region: "us-west3" + gcp_zone: "us-west3-b" \ No newline at end of file diff --git a/src/app_deploy/update-deploy-app.sh b/src/app_deploy/update-deploy-app.sh new file mode 100644 index 0000000..d3c7eea --- /dev/null +++ b/src/app_deploy/update-deploy-app.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# exit immediately if a command exits with a non-zero status +set -e + +# Define some environment variables +export IMAGE_NAME="app_deployment" +export BASE_DIR=$(pwd) +export SECRETS_DIR=$(pwd)/../../../secrets/ +export GCP_PROJECT="ac215project-398401" # Change to your GCP Project +export GCP_ZONE="us-west3-b" +export GOOGLE_APPLICATION_CREDENTIALS=/secrets/deployment.json + +# Build the image based on the Dockerfile +#docker build -t $IMAGE_NAME -f Dockerfile . +docker build -t $IMAGE_NAME --platform=linux/amd64 -f Dockerfile . + +# Run the container +docker run --rm --name $IMAGE_NAME \ +-v /var/run/docker.sock:/var/run/docker.sock \ +-v "$BASE_DIR":/app \ +-v "$SECRETS_DIR":/secrets \ +-v "$HOME/.ssh":/home/app/.ssh \ +-v "$BASE_DIR/../api-service":/api-service \ +-v "$BASE_DIR/../frontend":/frontend-react \ +-e GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS \ +-e USE_GKE_GCLOUD_AUTH_PLUGIN=True \ +-e GCP_PROJECT=$GCP_PROJECT \ +-e GCP_ZONE=$GCP_ZONE \ +$IMAGE_NAME ./deploy-app.sh \ No newline at end of file diff --git a/src/app_deploy/update-k8s-cluster.yml b/src/app_deploy/update-k8s-cluster.yml new file mode 100644 index 0000000..6e497e4 --- /dev/null +++ b/src/app_deploy/update-k8s-cluster.yml @@ -0,0 +1,82 @@ +--- +- name: "Update Kubernetes Cluster" + hosts: localhost + gather_facts: false + + vars: + cluster_name: "science-tutor-cluster" + + tasks: + - name: "Connect to cluster (update kubeconfig)" + shell: "gcloud container clusters get-credentials {{ cluster_name }} --zone {{ gcp_zone }} --project {{ gcp_project }}" + + - name: "Copy docker tag file" + copy: + src: .docker-tag + dest: .docker-tag + mode: 0644 + + - name: "Get docker tag" + shell: "cat .docker-tag" + register: tag + + - name: "Print tag" + debug: + var: tag + + - name: "Update Deployment for Frontend" + k8s: + state: present + definition: + apiVersion: v1 + kind: Deployment + metadata: + name: frontend + namespace: "{{cluster_name}}-namespace" + spec: + selector: + matchLabels: + run: frontend + template: + metadata: + labels: + run: frontend + spec: + containers: + - image: "gcr.io/{{ gcp_project }}/science-tutor-frontend-react:{{ tag.stdout}}" + imagePullPolicy: IfNotPresent + name: frontend + ports: + - containerPort: 80 + protocol: TCP + restartPolicy: Always + + - name: "Update Deployment for API Service" + k8s: + state: present + definition: + apiVersion: v1 + kind: Deployment + metadata: + name: api + namespace: "{{cluster_name}}-namespace" + spec: + selector: + matchLabels: + run: api + template: + metadata: + labels: + run: api + spec: + volumes: + - name: persistent-vol + emptyDir: {} + - name: google-cloud-key + secret: + secretName: gcp-service-key + containers: + - image: gcr.io/{{ gcp_project }}/science-tutor-api-service:{{ tag.stdout}} + imagePullPolicy: IfNotPresent + name: api + restartPolicy: Always \ No newline at end of file diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx index 2f27ad4..9b98b44 100644 --- a/src/frontend/src/App.jsx +++ b/src/frontend/src/App.jsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from "react"; import "./App.css"; import { Configuration, OpenAIApi } from "openai"; +const APP_VERSION = "1.0"; + // Reference: https://github.com/EBEREGIT/react-chatgpt-tutorial const configuration = new Configuration({ @@ -15,10 +17,23 @@ function App() { const [image, setImage] = useState(null); const [chats, setChats] = useState([]); const [isTyping, setIsTyping] = useState(false); + const [api_version, setAPIVersion] = useState(null); + const [torch_version, setTorchVersion] = useState(null); useEffect(() => { - const delayBeforeHello = 1000; // Adjust the delay in milliseconds + console.log("API Version", api_version, torch_version) + fetch("/api/status", { + "method": "GET", + }) + .then(response => response.json()) + .then(data => { + console.log(data); + setAPIVersion(data.api_version); + setTorchVersion(data.torch_version); + }) + .catch(error => console.error('Error:', error)); + const delayBeforeHello = 1000; // Adjust the delay in milliseconds setTimeout(() => { const systemMessage = "Hello! This is your Science Tutor. I can provide instant and expert answers to K12 science questions that you may have in different domains such as natural, social and language science. Feel free to ask me any questions you have and upload an image to start!"; @@ -83,8 +98,8 @@ function App() { setImage(null); console.log("Trying to send", chats, formData) - fetch("http://127.0.0.1:5000/chat", { - // fetch("http://34.125.158.148:5000/chat", { + fetch("/api/chat", { // for nginx + // fetch("http://127.0.0.1:5000/chat", { // for Docker.dev "method": "POST", body: formData }) @@ -115,6 +130,7 @@ function App() {

ScienceTutor

+

APP v{APP_VERSION}   API v{api_version}   PyTorch v{torch_version}

{chats && chats.length