Skip to content

Commit

Permalink
Move agent builds to a separate job
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayan-Bandyopadhyay committed Sep 17, 2024
1 parent 61f3975 commit 8fdbb49
Show file tree
Hide file tree
Showing 16 changed files with 1,801 additions and 52 deletions.
27 changes: 27 additions & 0 deletions deployer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM --platform=linux/amd64 python:3.11
ENV POETRY_HOME=/opt/poetry
ENV POETRY_VENV=/opt/poetry-venv
ENV POETRY_DATA_DIR=/opt/poetry-data
ENV POETRY_CONFIG_DIR=/opt/poetry-config
# Tell Poetry where to place its cache and virtual environment
ENV POETRY_CACHE_DIR=/opt/.cache

VOLUME ./.poetry-data-cache /opt/poetry-data
VOLUME ./.poetry-cache /opt/.cache

RUN python3.11 -m pip install poetry==1.7.1
WORKDIR /workspace
RUN poetry config virtualenvs.create false

COPY ./pyproject.toml ./pyproject.toml
COPY ./poetry.lock ./poetry.lock
# install only deps in dependency list first and lockfile to cache them
RUN poetry install --no-root --only main

COPY . .
# then install our own module
RUN poetry install --only main

EXPOSE 8080

ENTRYPOINT [ "poetry", "run", "start" ]
Empty file added deployer/README.md
Empty file.
1 change: 1 addition & 0 deletions deployer/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .database import Database
76 changes: 76 additions & 0 deletions deployer/database/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import Optional
import os
from supabase import create_client, Client
from models import AppConfig, Agent, User


class Database:
def __init__(self):
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
self.supabase = create_client(supabase_url, supabase_key)

def get_config(self, bearer_token: str) -> Optional[AppConfig]:
response = (
self.supabase.table("user")
.select("*")
.filter("secret_key", "eq", bearer_token)
.execute()
)
if len(response.data) > 0:
row = response.data[0]
return AppConfig(user_id=row["id"], app_id=row["app_id"])
return None

def get_secret_key_for_user(self, user_id: str):
response = (
self.supabase.table("user")
.select("secret_key")
.filter("id", "eq", user_id)
.execute()
)
if len(response.data) > 0:
return response.data[0]["secret_key"]
return None

def upsert_agent(self, agent: Agent) -> Optional[Agent]:
payload = agent.dict()
# Remove created_at field
payload.pop("created_at", None)
response = (
self.supabase.table("agent")
.upsert(
payload,
)
.execute()
)
if len(response.data) > 0:
row = response.data[0]
return Agent(**row)
return None

def get_agent(self, config: AppConfig, id: str) -> Optional[Agent]:
response = (
self.supabase.table("agent")
.select("*")
.filter("app_id", "eq", config.app_id)
.filter("id", "eq", id)
.execute()
)
if len(response.data) > 0:
row = response.data[0]
return Agent(**row)
return None

def get_user(self, config: AppConfig) -> Optional[Agent]:
response = (
self.supabase.table("user")
.select("*")
.filter("app_id", "eq", config.app_id)
.filter("id", "eq", config.user_id)
.execute()
)
if len(response.data) > 0:
row = response.data[0]
return User(**row)
return None
1 change: 1 addition & 0 deletions deployer/deployer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .deployer import Deployer
96 changes: 96 additions & 0 deletions deployer/deployer/deployer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from database import Database
from models import AppConfig, Agent
import os
import zipfile
from google.cloud.devtools import cloudbuild_v1
from google.oauth2 import service_account
import json
from datetime import timedelta
from google.cloud import run_v2


class Deployer:
def __init__(self):
service_account_string = os.getenv("GCLOUD_SERVICE_ACCOUNT")
self.deployments_bucket = os.getenv("DEPLOYMENTS_BUCKET")
self.project_id = os.getenv("GCLOUD_PROJECT")
credentials = service_account.Credentials.from_service_account_info(
json.loads(service_account_string)
)

self.build_client = cloudbuild_v1.CloudBuildClient(credentials=credentials)
self.jobs_client = run_v2.JobsClient(credentials=credentials)

def deploy_agent(self, agent: Agent):
# Check if the job already exists in Cloud Run
try:
self.jobs_client.get_job(
name=f"projects/{self.project_id}/locations/us-central1/jobs/{Agent.get_cloud_job_id(agent)}"
)
job_exists = True
except Exception:
job_exists = False

# Define the build steps
build_config = self._get_build_config(agent=agent, job_exists=job_exists)

# Trigger the build
build = cloudbuild_v1.Build(
steps=build_config["steps"],
images=build_config["images"],
source=cloudbuild_v1.Source(
storage_source=cloudbuild_v1.StorageSource(
bucket=self.deployments_bucket,
object_=f"{agent.finic_id}.zip",
)
),
)
operation = self.build_client.create_build(
project_id=self.project_id, build=build
)

# Wait for the build to complete
result = operation.result()
if result.status != cloudbuild_v1.Build.Status.SUCCESS:
raise Exception(f"Build failed with status: {result.status}")

print(f"Built and pushed Docker image: {agent.finic_id}")

def _get_build_config(self, agent: Agent, job_exists: bool) -> dict:
image_name = f"gcr.io/{self.project_id}/{agent.finic_id}:latest"
gcs_source = f"gs://{self.deployments_bucket}/{agent.finic_id}.zip"
job_command = "update" if job_exists else "create"
return {
"steps": [
{
"name": "gcr.io/cloud-builders/gsutil",
"args": ["cp", gcs_source, "/workspace/source.zip"],
},
{
"name": "gcr.io/cloud-builders/gcloud",
"entrypoint": "bash",
"args": [
"-c",
"apt-get update && apt-get install -y unzip && unzip /workspace/source.zip -d /workspace/unzipped",
],
},
{
"name": "gcr.io/cloud-builders/docker",
"args": ["build", "-t", image_name, "/workspace/unzipped"],
},
{
"name": "gcr.io/cloud-builders/docker",
"args": ["push", image_name],
},
{
"name": "gcr.io/google.com/cloudsdktool/cloud-sdk",
"entrypoint": "bash",
"args": [
"-c",
f"gcloud run jobs {job_command} {Agent.get_cloud_job_id(agent)} --image {image_name} --region us-central1 "
f"--tasks=1 --max-retries={agent.num_retries} --task-timeout=86400s --memory=4Gi",
],
},
],
"images": [image_name],
}
1 change: 1 addition & 0 deletions deployer/main/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .main import main
26 changes: 26 additions & 0 deletions deployer/main/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from database import Database
import os
from deployer.deployer import Deployer
from models.models import AgentStatus
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("FINIC_API_KEY")
AGENT_ID = os.getenv("FINIC_AGENT_ID")


def main():
db = Database()
config = db.get_config(API_KEY)
agent = db.get_agent(config=config, id=AGENT_ID)
try:
deployer = Deployer()
deployer.deploy_agent(agent=agent)
agent.status = AgentStatus.deployed
db.upsert_agent(agent)
return agent
except Exception as e:
agent.status = AgentStatus.failed
db.upsert_agent(agent)
raise e
1 change: 1 addition & 0 deletions deployer/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .models import AppConfig, Agent, User, AgentStatus, ExecutionStatus
43 changes: 43 additions & 0 deletions deployer/models/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from enum import Enum
import datetime
from typing import Optional
from pydantic import BaseModel


class AppConfig(BaseModel):
user_id: str
app_id: str


class User(BaseModel):
id: str
created_at: datetime.datetime
email: str
secret_key: str
avatar_url: str


class AgentStatus(str, Enum):
deploying = "deploying"
deployed = "deployed"
failed = "deploy_failed"


class ExecutionStatus(str, Enum):
running = "running"
successful = "successful"
failed = "failed"


class Agent(BaseModel):
finic_id: str
id: str
app_id: str
description: str
status: AgentStatus
created_at: Optional[datetime.datetime] = None
num_retries: int = 3

@staticmethod
def get_cloud_job_id(agent: "Agent") -> str:
return f"job-{agent.finic_id}"
Loading

0 comments on commit 8fdbb49

Please sign in to comment.