diff --git a/agenta-backend/agenta_backend/models/converters.py b/agenta-backend/agenta_backend/models/converters.py
index 834edd179d..731b4bda97 100644
--- a/agenta-backend/agenta_backend/models/converters.py
+++ b/agenta-backend/agenta_backend/models/converters.py
@@ -5,7 +5,7 @@
def app_variant_db_to_pydantic(app_variant_db: AppVariantDB, previous_variant_name: str = None) -> AppVariant:
- return AppVariant(app_name=app_variant_db.app_name, variant_name=app_variant_db.variant_name, parameters=app_variant_db.parameters, previous_variant_name=previous_variant_name)
+ return AppVariant(app_name=app_variant_db.app_name, variant_name=app_variant_db.variant_name, parameters=app_variant_db.parameters, previous_variant_name=app_variant_db.previous_variant_name)
def image_db_to_pydantic(image_db: ImageDB) -> Image:
diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py
index 02d7783827..ee02f640ab 100644
--- a/agenta-backend/agenta_backend/models/db_models.py
+++ b/agenta-backend/agenta_backend/models/db_models.py
@@ -16,5 +16,5 @@ class AppVariantDB(SQLModel, table=True):
variant_name: str = Field(...)
image_id: int = Field(foreign_key="imagedb.id")
parameters: Dict = Field(sa_column=Column(JSON))
- previous_variant_id: Optional[int] = Field(default=None, foreign_key="appvariantdb.id")
- version: int = Field(default=1)
+ previous_variant_name: Optional[str] = Field(default=None)
+ is_deleted: bool = Field(default=False) # soft deletion for using the template variants
diff --git a/agenta-backend/agenta_backend/routers/app_variant.py b/agenta-backend/agenta_backend/routers/app_variant.py
index f0003e9e16..f8bd107020 100644
--- a/agenta-backend/agenta_backend/routers/app_variant.py
+++ b/agenta-backend/agenta_backend/routers/app_variant.py
@@ -6,10 +6,14 @@
from agenta_backend.config import settings
from agenta_backend.models.api.api_models import URI, App, AppVariant, Image
-from agenta_backend.services import db_manager, docker_utils
-from fastapi import APIRouter, HTTPException, Body
+from agenta_backend.services import app_manager, db_manager, docker_utils
+from docker.errors import DockerException
+from fastapi import APIRouter, Body, HTTPException
+from sqlalchemy.exc import SQLAlchemyError
router = APIRouter()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
@router.get("/list_variants/", response_model=List[AppVariant])
@@ -42,7 +46,7 @@ async def list_apps() -> List[App]:
List[App]
"""
try:
- apps = db_manager.list_app_names()
+ apps = db_manager.list_apps()
return apps
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -98,10 +102,7 @@ async def add_variant_from_previous(previous_app_variant: AppVariant, new_varian
@router.post("/start/")
async def start_variant(app_variant: AppVariant) -> URI:
try:
- image: Image = db_manager.get_image(app_variant)
- uri: URI = docker_utils.start_container(
- image_name=image.tags, app_name=app_variant.app_name, variant_name=app_variant.variant_name)
- return uri
+ return app_manager.start_variant(app_variant)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -131,6 +132,7 @@ async def list_images():
@router.delete("/remove_variant/")
async def remove_variant(app_variant: AppVariant):
"""Remove a variant from the server.
+ In the case it's the last variant using the image, stop the container and remove the image.
Arguments:
app_variant -- AppVariant to remove
@@ -139,7 +141,33 @@ async def remove_variant(app_variant: AppVariant):
HTTPException: If there is a problem removing the app variant
"""
try:
- if not db_manager.remove_app_variant(app_variant):
- raise HTTPException(status_code=404, detail="App variant not found")
+ app_manager.remove_app_variant(app_variant)
+ except SQLAlchemyError as e:
+ detail = f"Database error while trying to remove the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except DockerException as e:
+ detail = f"Docker error while trying to remove the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
+ detail = f"Unexpected error while trying to remove the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+
+
+@router.delete("/remove_app/")
+async def remove_app(app: App):
+ """Remove app, all its variant, containers and images
+
+ Arguments:
+ app -- App to remove
+ """
+ try:
+ app_manager.remove_app(app)
+ except SQLAlchemyError as e:
+ detail = f"Database error while trying to remove the app: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except DockerException as e:
+ detail = f"Docker error while trying to remove the app: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except Exception as e:
+ detail = f"Unexpected error while trying to remove the app: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py
new file mode 100644
index 0000000000..edc603b30a
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/app_manager.py
@@ -0,0 +1,124 @@
+"""Main Business logic
+"""
+
+from agenta_backend.services import (db_manager, docker_utils)
+from agenta_backend.models.api.api_models import (AppVariant, Image, URI, App)
+import logging
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def remove_app_variant(app_variant: AppVariant):
+ """Removes appvariant from db, if it is the last one using an image, then
+ deletes the image from the db, shutdowns the container, deletes it and remove
+ the image from the registry
+
+ Arguments:
+ app_variant -- the app variant to remove
+ """
+ # checks if it is the last app variant using its image
+ try:
+ app_variant_db = db_manager.get_variant_from_db(app_variant)
+ except Exception as e:
+ logger.error(f"Error fetching app variant from the database: {str(e)}")
+ raise
+
+ if app_variant_db is None:
+ msg = f"App variant {app_variant.app_name}/{app_variant.variant_name} not found in DB"
+ logger.error(msg)
+ raise ValueError(msg)
+ else:
+ try:
+ if db_manager.check_is_last_variant(app_variant_db):
+ image: Image = db_manager.get_image(app_variant)
+ try:
+ container_ids = docker_utils.stop_containers_based_on_image(image)
+ logger.info(f"Containers {container_ids} stopped")
+ for container_id in container_ids:
+ docker_utils.delete_container(container_id)
+ logger.info(f"Container {container_id} deleted")
+ except Exception as e:
+ logger.error(f"Error managing Docker resources: {str(e)}")
+ raise
+ try:
+ docker_utils.delete_image(image)
+ logger.info(f"Image {image.tags} deleted")
+ except:
+ logger.warning(f"Warning: Error deleting image {image.tags}. Probably multiple variants using it.")
+ db_manager.remove_image(image)
+ db_manager.remove_app_variant(app_variant)
+ except Exception as e:
+ logger.error(f"Error deleting app variant: {str(e)}")
+ raise
+
+
+def remove_app(app: App):
+ """Removes all app variants from db, if it is the last one using an image, then
+ deletes the image from the db, shutdowns the container, deletes it and remove
+ the image from the registry
+
+ Arguments:
+ app_name -- the app name to remove
+ """
+ # checks if it is the last app variant using its image
+ app_name = app.app_name
+ if app_name not in [app.app_name for app in db_manager.list_apps()]:
+ msg = f"App {app_name} not found in DB"
+ logger.error(msg)
+ raise ValueError(msg)
+ try:
+ app_variants = db_manager.list_app_variants(app_name=app_name, show_soft_deleted=True)
+ except Exception as e:
+ logger.error(f"Error fetching app variants from the database: {str(e)}")
+ raise
+ if app_variants is None:
+ msg = f"App {app_name} not found in DB"
+ logger.error(msg)
+ raise ValueError(msg)
+ else:
+ try:
+ for app_variant in app_variants:
+ remove_app_variant(app_variant)
+ except Exception as e:
+ logger.error(f"Error deleting app variants: {str(e)}")
+ raise
+
+
+def start_variant(app_variant: AppVariant) -> URI:
+ """
+ Starts a Docker container for a given app variant.
+
+ Fetches the associated image from the database and delegates to a Docker utility function
+ to start the container. The URI of the started container is returned.
+
+ Args:
+ app_variant (AppVariant): The app variant for which a container is to be started.
+
+ Returns:
+ URI: The URI of the started Docker container.
+
+ Raises:
+ ValueError: If the app variant does not have a corresponding image in the database.
+ RuntimeError: If there is an error starting the Docker container.
+ """
+ try:
+ image: Image = db_manager.get_image(app_variant)
+ except Exception as e:
+ logger.error(
+ f"Error fetching image for app variant {app_variant.app_name}/{app_variant.variant_name} from database: {str(e)}")
+ raise ValueError(
+ f"Image for app variant {app_variant.app_name}/{app_variant.variant_name} not found in database") from e
+
+ try:
+ uri: URI = docker_utils.start_container(
+ image_name=image.tags, app_name=app_variant.app_name, variant_name=app_variant.variant_name)
+ logger.info(
+ f"Started Docker container for app variant {app_variant.app_name}/{app_variant.variant_name} at URI {uri}")
+ except Exception as e:
+ logger.error(
+ f"Error starting Docker container for app variant {app_variant.app_name}/{app_variant.variant_name}: {str(e)}")
+ raise RuntimeError(
+ f"Failed to start Docker container for app variant {app_variant.app_name}/{app_variant.variant_name}") from e
+
+ return uri
diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py
index 8ed50c2bb0..fcb843e1e8 100644
--- a/agenta-backend/agenta_backend/services/db_manager.py
+++ b/agenta-backend/agenta_backend/services/db_manager.py
@@ -5,14 +5,18 @@
from agenta_backend.models.converters import (app_variant_db_to_pydantic,
image_db_to_pydantic)
from agenta_backend.models.db_models import AppVariantDB, ImageDB
+from agenta_backend.services import helpers
from sqlmodel import Session, SQLModel, create_engine, func, and_
-
+import logging
# SQLite database connection
DATABASE_URL = os.environ["DATABASE_URL"]
engine = create_engine(DATABASE_URL)
# Create tables if they don't exist
SQLModel.metadata.create_all(engine)
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
def get_session():
"""Returns a session to the database
@@ -24,34 +28,6 @@ def get_session():
yield session
-def add_app_variant(app_variant: AppVariant, image: Image):
- """Adds a new app variant to the db.
- First adds an app variant, then adds the image to the db and links it to the app variant
-
- Arguments:
- app_variant -- AppVariant to add
- image -- The Image associated with the app variant
- """
- if app_variant is None or image is None or app_variant.app_name in [None, ""] or app_variant.variant_name in [None, ""] or image.docker_id in [None, ""] or image.tags in [None, ""]:
- raise ValueError("App variant or image is None")
- already_exists = any([av for av in list_app_variants() if av.app_name ==
- app_variant.app_name and av.variant_name == app_variant.variant_name])
- if already_exists:
- raise ValueError("App variant already exists")
- with Session(engine) as session:
- # Add image
- db_image = ImageDB(**image.dict())
- session.add(db_image)
- session.commit()
- session.refresh(db_image)
- # Add app variant and link it to the app variant
- db_app_variant = AppVariantDB(
- image_id=db_image.id, **app_variant.dict())
- session.add(db_app_variant)
- session.commit()
- session.refresh(db_app_variant)
-
-
def add_variant_based_on_image(app_variant: AppVariant, image: Image):
"""Adds an app variant based on an image. This the functionality called by the cli.
Currently we are not using the parameters field, but it is there for future use.
@@ -63,11 +39,12 @@ def add_variant_based_on_image(app_variant: AppVariant, image: Image):
Raises:
ValueError: if variant exists or missing inputs
"""
+ clean_soft_deleted_variants()
if app_variant is None or image is None or app_variant.app_name in [None, ""] or app_variant.variant_name in [None, ""] or image.docker_id in [None, ""] or image.tags in [None, ""]:
raise ValueError("App variant or image is None")
if app_variant.parameters is not None:
raise ValueError("Parameters are not supported when adding based on image")
- already_exists = any([av for av in list_app_variants() if av.app_name ==
+ already_exists = any([av for av in list_app_variants(show_soft_deleted=True) if av.app_name ==
app_variant.app_name and av.variant_name == app_variant.variant_name])
if already_exists:
raise ValueError("App variant with the same name already exists")
@@ -97,6 +74,7 @@ def add_variant_based_on_previous(previous_app_variant: AppVariant, new_variant_
Raises:
ValueError: _description_
"""
+ clean_soft_deleted_variants()
if previous_app_variant is None or previous_app_variant.app_name in [None, ""] or previous_app_variant.variant_name in [None, ""]:
raise ValueError("App variant is None")
if parameters is None:
@@ -108,12 +86,13 @@ def add_variant_based_on_previous(previous_app_variant: AppVariant, new_variant_
(AppVariantDB.app_name == previous_app_variant.app_name) & (AppVariantDB.variant_name == previous_app_variant.variant_name)).first()
if template_variant is None:
+ print_all()
raise ValueError("Template app variant not found")
- elif template_variant.previous_variant_id is not None:
+ elif template_variant.previous_variant_name is not None:
raise ValueError(
"Template app variant is not a template, it is a forked variant itself")
- already_exists = any([av for av in list_app_variants() if av.app_name ==
+ already_exists = any([av for av in list_app_variants(show_soft_deleted=True) if av.app_name ==
previous_app_variant.app_name and av.variant_name == new_variant_name])
if already_exists:
raise ValueError("App variant with the same name already exists")
@@ -124,59 +103,51 @@ def add_variant_based_on_previous(previous_app_variant: AppVariant, new_variant_
variant_name=new_variant_name,
image_id=template_variant.image_id,
parameters=parameters,
- previous_variant_id=template_variant.id,
- version=template_variant.version + 1)
+ previous_variant_name=template_variant.variant_name)
session.add(db_app_variant)
session.commit()
session.refresh(db_app_variant)
-def list_app_variants(app_name: str = None) -> List[AppVariant]:
+def list_app_variants(app_name: str = None, show_soft_deleted=False) -> List[AppVariant]:
"""
- Lists all the app variants from the db, only latest versions
- TODO: TEST THIS
-
+ Lists all the app variants from the db
+ Args:
+ app_name: if specified, only returns the variants for the app name
+ show_soft_deleted: if true, returns soft deleted variants as well
+ Returns:
+ List[AppVariant]: List of AppVariant objects
"""
-
+ clean_soft_deleted_variants()
with Session(engine) as session:
query = session.query(AppVariantDB)
+ if not show_soft_deleted:
+ query = query.filter(AppVariantDB.is_deleted == False)
if app_name is not None:
query = query.filter(AppVariantDB.app_name == app_name)
- # Get latest versions only
- subquery = session.query(AppVariantDB.app_name, AppVariantDB.variant_name, func.max(AppVariantDB.version).label("max_version"))\
- .group_by(AppVariantDB.app_name, AppVariantDB.variant_name).subquery()
+ subquery = session.query(AppVariantDB.app_name, AppVariantDB.variant_name).group_by(
+ AppVariantDB.app_name, AppVariantDB.variant_name).subquery()
query = query.join(subquery, and_(AppVariantDB.app_name == subquery.c.app_name,
- AppVariantDB.variant_name == subquery.c.variant_name,
- AppVariantDB.version == subquery.c.max_version))
+ AppVariantDB.variant_name == subquery.c.variant_name))
app_variants_db: List[AppVariantDB] = query.all()
# Include previous variant name
app_variants: List[AppVariant] = []
for av in app_variants_db:
- if av.previous_variant_id is None:
- app_variant = app_variant_db_to_pydantic(av)
- else:
- previous_variant = session.query(AppVariantDB).filter(AppVariantDB.id == av.previous_variant_id).first()
-
- if previous_variant:
- app_variant = app_variant_db_to_pydantic(av, previous_variant.variant_name)
- else:
- raise ValueError("Previous variant not found!!")
+ app_variant = app_variant_db_to_pydantic(av)
app_variants.append(app_variant)
-
return app_variants
-def list_app_names() -> List[App]:
+def list_apps() -> List[App]:
"""
Lists all the unique app names from the database
"""
-
+ clean_soft_deleted_variants()
with Session(engine) as session:
app_names = session.query(AppVariantDB.app_name).distinct().all()
-
# Unpack tuples to create a list of strings instead of a list of tuples
return [App(app_name=name) for (name,) in app_names]
@@ -193,7 +164,7 @@ def get_image(app_variant: AppVariant) -> Image:
with Session(engine) as session:
db_app_variant: AppVariantDB = session.query(AppVariantDB).filter(
- (AppVariantDB.app_name == app_variant.app_name) & (AppVariantDB.variant_name == app_variant.variant_name)).order_by(AppVariantDB.version.desc()).first()
+ (AppVariantDB.app_name == app_variant.app_name) & (AppVariantDB.variant_name == app_variant.variant_name)).first()
if db_app_variant:
image_db: ImageDB = session.query(ImageDB).filter(
ImageDB.id == db_app_variant.image_id).first()
@@ -202,32 +173,112 @@ def get_image(app_variant: AppVariant) -> Image:
raise Exception("App variant not found")
-def remove_app_variant(app_variant: AppVariant) -> bool:
- """Remove an app variant and its associated image from the db
- in case it is the only variant in the db using the image
+def remove_app_variant(app_variant: AppVariant):
+ """Remove an app variant from the db
+ the logic for removing the image is in app_manager.py
Arguments:
app_variant -- AppVariant to remove
+ """
+ if app_variant is None or app_variant.app_name in [None, ""] or app_variant.variant_name in [None, ""]:
+ raise ValueError("App variant is None")
+ with Session(engine) as session:
+ app_variant_db = session.query(AppVariantDB).filter(
+ (AppVariantDB.app_name == app_variant.app_name) & (AppVariantDB.variant_name == app_variant.variant_name)).first()
+ if app_variant_db is None:
+ raise ValueError("App variant not found")
+
+ if app_variant_db.previous_variant_name is not None: # forked variant
+ session.delete(app_variant_db)
+ elif check_is_last_variant(app_variant_db): # last variant using the image, okay to delete
+ session.delete(app_variant_db)
+ else:
+ app_variant_db.is_deleted = True # soft deletion
+ session.commit()
+
+
+def remove_image(image: Image):
+ """Remove image from db based on pydantic class
+
+ Arguments:
+ image -- Image to remove
+ """
+ if image is None or image.docker_id in [None, ""] or image.tags in [None, ""]:
+ raise ValueError("Image is None")
+ with Session(engine) as session:
+ image_db = session.query(ImageDB).filter(
+ (ImageDB.docker_id == image.docker_id) & (ImageDB.tags == image.tags)).first()
+ if image_db is None:
+ raise ValueError("Image not found")
+ session.delete(image_db)
+ session.commit()
+
+
+def check_is_last_variant(db_app_variant: AppVariantDB) -> bool:
+ """Checks whether the input variant is the sole variant that uses its linked image
+ This is a helpful function to determine whether to delete the image when removing a variant
+ Usually many variants will use the same image (these variants would have been created using the UI)
+ We only delete the image and shutdown the container if the variant is the last one using the image
+
+ Arguments:
+ app_variant -- AppVariant to check
Returns:
- bool -- True if the app variant was removed, False otherwise
+ true if it's the last variant, false otherwise
"""
+ with Session(engine) as session:
+ # If it's the only variant left that uses the image, delete the image
+ if session.query(AppVariantDB).filter(AppVariantDB.image_id == db_app_variant.image_id).count() == 1:
+ return True
+ else:
+ return False
+
+
+def get_variant_from_db(app_variant: AppVariant) -> AppVariantDB:
+ """Checks whether the app variant exists in our db
+ and returns the AppVariantDB object if it does
+
+ Arguments:
+ app_variant -- AppVariant to check
+ Returns:
+ AppVariantDB -- The AppVariantDB object if it exists, None otherwise
+ """
with Session(engine) as session:
# Find app_variant in the database
db_app_variant: AppVariantDB = session.query(AppVariantDB).filter(
- (AppVariantDB.app_name == app_variant.app_name) & (AppVariantDB.variant_name == app_variant.variant_name)).order_by(AppVariantDB.version.desc()).first()
+ (AppVariantDB.app_name == app_variant.app_name) & (AppVariantDB.variant_name == app_variant.variant_name)).first()
+ logger.info(f"Found app variant: {db_app_variant}")
if db_app_variant:
- # If it's the original variant, delete associated image
- if session.query(AppVariantDB).filter(AppVariantDB.image_id == db_app_variant.image_id).count() == 1:
- db_image: ImageDB = session.query(ImageDB).filter(
- ImageDB.id == db_app_variant.image_id).first()
- if db_image:
- session.delete(db_image)
- else:
- raise Exception("Image for app not found")
- # Delete app_variant
- session.delete(db_app_variant)
- session.commit()
- return True
+ return db_app_variant
else:
- return False
+ return None
+
+
+def print_all():
+ """Prints all the tables in the database
+ """
+ with Session(engine) as session:
+ for app_variant in session.query(AppVariantDB).all():
+ helpers.print_app_variant(app_variant)
+ for image in session.query(ImageDB).all():
+ helpers.print_image(image)
+
+
+def clean_soft_deleted_variants():
+ """Remove soft-deleted app variants if their image is not used by any existing variant.
+ """
+ with Session(engine) as session:
+ # Get all soft-deleted app variants
+ soft_deleted_variants: List[AppVariantDB] = session.query(
+ AppVariantDB).filter(AppVariantDB.is_deleted == True).all()
+
+ for variant in soft_deleted_variants:
+ # Get non-deleted variants that use the same image
+ image_used = session.query(AppVariantDB).filter(
+ (AppVariantDB.image_id == variant.image_id) & (AppVariantDB.is_deleted == False)).first()
+
+ # If the image is not used by any non-deleted variant, delete the variant
+ if image_used is None:
+ session.delete(variant)
+
+ session.commit()
diff --git a/agenta-backend/agenta_backend/services/docker_utils.py b/agenta-backend/agenta_backend/services/docker_utils.py
index 230a7dd0d8..ea136cf1e3 100644
--- a/agenta-backend/agenta_backend/services/docker_utils.py
+++ b/agenta-backend/agenta_backend/services/docker_utils.py
@@ -3,10 +3,14 @@
import docker
from agenta_backend.config import settings
from agenta_backend.models.api.api_models import AppVariant, Image, URI
-
+import logging
client = docker.from_env()
+# Set up logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
def port_generator(start_port=9000):
port = start_port
@@ -49,41 +53,65 @@ def start_container(image_name, app_name, variant_name) -> URI:
f"traefik.http.routers.{app_name}-{variant_name}.service": f"{app_name}-{variant_name}",
}
container = client.containers.run(
- image, detach=True, labels=labels, network="agenta-network")
+ image, detach=True, labels=labels, network="agenta-network", name=f"{app_name}-{variant_name}")
return URI(uri=f"http://localhost/{app_name}/{variant_name}")
-def stop_container(container_id):
- container = client.containers.get(container_id)
- response = container.stop()
- return response
-
-
-def delete_container(container_id):
- container = client.containers.get(container_id)
- response = container.remove()
- return response
-
+def stop_containers_based_on_image(image: Image) -> List[str]:
+ """Stops all the containers that use a certain image
-# def list_images():
-# images = client.images.list()
-# return images
+ Arguments:
+ image -- Image containing the docker id
+ Raises:
+ RuntimeError: _description_
-# def pull_image(image_name, tag="latest"):
-# image = client.images.pull(
-# f"{settings.docker_registry_url}/{image_name}:{tag}")
-# return image
+ Returns:
+ The container ids of the stopped containers
+ """
+ stopped_container_ids = []
+ for container in client.containers.list(all=True):
+ if container.image.id == image.docker_id:
+ try:
+ container.stop()
+ stopped_container_ids.append(container.id)
+ logger.info(f'Stopped container with id: {container.id}')
+ except docker.errors.APIError as ex:
+ logger.error(f'Error stopping container with id: {container.id}. Error: {str(ex)}')
+ raise RuntimeError(f'Error stopping container with id: {container.id}') from ex
+ return stopped_container_ids
+
+
+def delete_container(container_id: str):
+ """Delete a container based on its id
+
+ Arguments:
+ container_id -- _description_
+
+ Raises:
+ RuntimeError: _description_
+ """
+ try:
+ container = client.containers.get(container_id)
+ container.remove()
+ logger.info(f'Deleted container with id: {container.id}')
+ except docker.errors.APIError as ex:
+ logger.error(f'Error deleting container with id: {container.id}. Error: {str(ex)}')
+ raise RuntimeError(f'Error deleting container with id: {container.id}') from ex
-# def push_image(image_name, tag="latest"):
-# image = client.images.get(f"{image_name}:{tag}")
-# response = client.images.push(
-# f"{settings.docker_registry_url}/{image_name}", tag=tag)
-# return response
+def delete_image(image: Image):
+ """Delete an image based on its id
+ Arguments:
+ image -- _description_
-# def delete_image(image_name, tag="latest"):
-# image = client.images.get(f"{image_name}:{tag}")
-# response = client.images.remove(image.id)
-# return response
+ Raises:
+ RuntimeError: _description_
+ """
+ try:
+ client.images.remove(image.docker_id)
+ logger.info(f'Deleted image with id: {image.docker_id}')
+ except docker.errors.APIError as ex:
+ logger.error(f'Error deleting image with id: {image.docker_id}. Error: {str(ex)}')
+ raise RuntimeError(f'Error deleting image with id: {image.docker_id}') from ex
diff --git a/agenta-backend/agenta_backend/services/helpers.py b/agenta-backend/agenta_backend/services/helpers.py
new file mode 100644
index 0000000000..55c2ddf66a
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/helpers.py
@@ -0,0 +1,17 @@
+
+def print_app_variant(app_variant):
+ print(f"App Variant ID: {app_variant.id}")
+ print(f"App Variant Name: {app_variant.variant_name}")
+ print(f"App Name: {app_variant.app_name}")
+ print(f"Image ID: {app_variant.image_id}")
+ print(f"Parameters: {app_variant.parameters}")
+ print(f"Previous Variant Name: {app_variant.previous_variant_name}")
+ print(f"Is Deleted: {app_variant.is_deleted}")
+ print("------------------------")
+
+
+def print_image(image):
+ print(f"Image ID: {image.id}")
+ print(f"Docker ID: {image.docker_id}")
+ print(f"Tags: {image.tags}")
+ print("------------------------")
diff --git a/agenta-backend/tests/manual_tests.py b/agenta-backend/tests/manual_tests.py
index ea631d55c2..d953a857b0 100644
--- a/agenta-backend/tests/manual_tests.py
+++ b/agenta-backend/tests/manual_tests.py
@@ -2,11 +2,13 @@
import docker
from agenta_backend.config import settings
-from agenta_backend.services.docker_utils import list_images, start_container, stop_container, delete_container
-from agenta_backend.services.db_manager import add_app_variant, list_app_variants, get_image
-from agenta_backend.models.api_models import AppVariant, Image, URI
+from agenta_backend.services import app_manager
+from agenta_backend.services import db_manager
+from agenta_backend.models.api.api_models import AppVariant, Image, URI
-client = docker.from_env()
-uri = start_container("agenta-server/clitest", "clitest")
-
-print(uri)
+db_manager.print_all()
+app_manager.remove_app("baby_name_generator")
+# app = AppVariant(app_name="baby_name_generator", variant_name="v0")
+# app_manager.remove_app_variant(app)
+# app = AppVariant(app_name="baby_name_generator", variant_name="v0.2")
+# app_manager.remove_app_variant(app)
diff --git a/agenta-backend/tests/test_db_manager.py b/agenta-backend/tests/test_db_manager.py
index 8f7d84db4e..26a16bf928 100644
--- a/agenta-backend/tests/test_db_manager.py
+++ b/agenta-backend/tests/test_db_manager.py
@@ -5,11 +5,12 @@
from agenta_backend.models.api.api_models import App, AppVariant, Image
from agenta_backend.services.db_manager import (add_variant_based_on_image,
engine, get_image, get_session,
- list_app_names,
+ list_apps,
list_app_variants,
remove_app_variant,
- add_variant_based_on_previous)
+ add_variant_based_on_previous, print_all)
from sqlmodel import Session
+from time import sleep
@pytest.fixture(autouse=True)
@@ -89,7 +90,8 @@ def test_add_same_app_variant_twice(app_variant, image):
def test_remove_non_existent_app_variant():
non_existent_app_variant = AppVariant(
app_name=random_string(), variant_name=random_string())
- remove_app_variant(non_existent_app_variant) # Should not raise an error
+ with pytest.raises(ValueError):
+ remove_app_variant(non_existent_app_variant) # Should not raise an error
assert len(list_app_variants()) == 0
@@ -161,7 +163,7 @@ def test_filter_by_app_name(image: Image):
assert app_variant.app_name == app_name
-def test_list_app_names(image):
+def test_list_apps(image):
# Assuming you have a setUp function that clears the database before each test
app_name1 = 'test_app'
app_name2 = 'other_app'
@@ -176,8 +178,8 @@ def test_list_app_names(image):
variant_name = ''.join(choice(ascii_letters) for _ in range(10))
add_variant_based_on_image(AppVariant(app_name=app_name2, variant_name=variant_name), image)
- # Check that list_app_names returns all unique app names
- app_names = list_app_names()
+ # Check that list_apps returns all unique app names
+ app_names = list_apps()
assert len(app_names) == 2
assert App(app_name=app_name1) in app_names
assert App(app_name=app_name2) in app_names
@@ -269,16 +271,20 @@ def test_add_remove_chain_of_variants(app_variant, image):
# Add another variant based on the previous one
new_variant2 = AppVariant(app_name=app_variant.app_name, variant_name=random_string(),
parameters={'param1': 'value2', 'param2': 20})
- add_variant_based_on_previous(previous_app_variant=new_variant1,
+ with pytest.raises(ValueError):
+ add_variant_based_on_previous(previous_app_variant=new_variant1,
+ new_variant_name=new_variant2.variant_name,
+ parameters=new_variant2.parameters)
+ add_variant_based_on_previous(previous_app_variant=app_variant,
new_variant_name=new_variant2.variant_name,
parameters=new_variant2.parameters)
+
assert get_image(new_variant1) is not None
# Remove original variant
remove_app_variant(app_variant)
# Image should still exist as other variants are using it
- with pytest.raises(Exception):
- get_image(app_variant)
+ get_image(app_variant)
assert get_image(new_variant1) is not None
@@ -296,3 +302,48 @@ def test_add_remove_chain_of_variants(app_variant, image):
# Now the image should not exist as all variants using it have been removed
with pytest.raises(Exception):
get_image(new_variant2)
+
+
+def test_add_variant_based_on_previous(app_variant, app_variant2, image):
+ add_variant_based_on_image(app_variant, image)
+ parameters = {"key": "value"}
+ add_variant_based_on_previous(previous_app_variant=app_variant,
+ new_variant_name=app_variant2.variant_name, parameters=parameters)
+ app_variants = list_app_variants()
+ assert len(app_variants) == 2
+ assert app_variants[1].app_name == app_variant.app_name
+ assert app_variants[1].variant_name == app_variant2.variant_name
+
+
+def test_remove_app_variant_and_check_soft_deletion(app_variant, app_variant2, image):
+ add_variant_based_on_image(app_variant, image)
+ parameters = {"key": "value"}
+ add_variant_based_on_previous(app_variant, app_variant2.variant_name, parameters)
+ remove_app_variant(app_variant)
+ app_variants = list_app_variants(show_soft_deleted=True)
+ assert len(app_variants) == 2
+ app_variants = list_app_variants()
+ assert len(app_variants) == 1
+
+
+def test_add_variant_after_remove(app_variant, app_variant2, image):
+ add_variant_based_on_image(app_variant, image)
+ remove_app_variant(app_variant)
+ parameters = {"key": "value"}
+ with pytest.raises(ValueError):
+ add_variant_based_on_previous(previous_app_variant=app_variant,
+ new_variant_name=app_variant2.variant_name, parameters=parameters)
+
+
+def test_add_variant_based_on_previous_with_soft_deleted_variant(app_variant, app_variant2, image):
+ add_variant_based_on_image(app_variant, image)
+ parameters = {"key": "value"}
+ add_variant_based_on_previous(app_variant, app_variant2.variant_name+"2", parameters)
+ remove_app_variant(app_variant)
+
+ add_variant_based_on_previous(app_variant, app_variant2.variant_name, parameters)
+ app_variants = list_app_variants()
+ print_all()
+ assert len(app_variants) == 2
+ assert app_variants[1].app_name == app_variant.app_name
+ assert app_variants[1].variant_name == app_variant2.variant_name
diff --git a/agenta-backend/tests/test_router_app_variant.py b/agenta-backend/tests/test_router_app_variant.py
index 5ac4decb4d..263f79fd6c 100644
--- a/agenta-backend/tests/test_router_app_variant.py
+++ b/agenta-backend/tests/test_router_app_variant.py
@@ -6,7 +6,7 @@
import pytest
from agenta_backend.main import app
from agenta_backend.models.api.api_models import AppVariant, Image
-from agenta_backend.services.db_manager import (add_app_variant, engine,
+from agenta_backend.services.db_manager import (add_variant_based_on_image, engine,
get_image, get_session,
list_app_variants,
remove_app_variant)
@@ -79,7 +79,7 @@ def test_list_app_variant():
def test_list_app_variant_after_manual_add(app_variant, image):
# This is the function from db_manager.py
- add_app_variant(app_variant, image)
+ add_variant_based_on_image(app_variant, image)
response = client.get("/app_variant/list_variants/")
assert response.status_code == 200
assert len(response.json()) == 1
diff --git a/agenta-web/src/components/AppSelector/AppCard.tsx b/agenta-web/src/components/AppSelector/AppCard.tsx
new file mode 100644
index 0000000000..c6d3e163be
--- /dev/null
+++ b/agenta-web/src/components/AppSelector/AppCard.tsx
@@ -0,0 +1,85 @@
+import { Modal, Tooltip, Card } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+import { removeApp } from '@/lib/services/api';
+import useSWR, { mutate } from 'swr';
+import { useState } from 'react';
+import Link from 'next/link';
+
+const DeleteModal = ({ visible, handleOk, handleCancel, appName, confirmLoading }) => {
+ return (
+
+