diff --git a/api_client/client.py b/api_client/client.py index 2f04148..62b8be7 100644 --- a/api_client/client.py +++ b/api_client/client.py @@ -299,4 +299,19 @@ class IaaSClient: list: List of label dictionaries or None if error occurs """ endpoint = f'workload_hosts/{host_id}/labels' - return self._make_request('GET', endpoint) \ No newline at end of file + return self._make_request('GET', endpoint) + + + # --- Container Pod CRUD --- + + def get_container_pods(self): + """Fetch all container pods""" + return self._make_request('GET', 'workloads/pods') + + def get_container_pod(self, pod_id): + """Fetch a single container pod by ID""" + return self._make_request('GET', f'workloads/pods/{pod_id}') + + def delete_container_pod(self, pod_id): + """Delete (mark pending-deleted) a container pod""" + return self._make_request('DELETE', f'workloads/pods/{pod_id}') diff --git a/app/controller/api/workload_container_routes.py b/app/controller/api/workload_container_routes.py index c0002b8..d30327b 100644 --- a/app/controller/api/workload_container_routes.py +++ b/app/controller/api/workload_container_routes.py @@ -2,7 +2,7 @@ import random from flask import json, request, jsonify, abort import requests from app import app, db, logger -from app.models.models import Workload, WorkloadHost, WorkloadHostPortMapping, VirtualDataCenter, Label, VolumeWorkloadMapping, ContainerPod, ContainerPodContainer +from app.models.models import Workload, WorkloadHost, WorkloadHostPortMapping, VirtualDataCenter, Label, VolumeWorkloadMapping, ContainerPod, ContainerPodContainer, CloudflareDNSRecord, CloudflareTunnel from app.models.network import Network, NetworkPort from datetime import datetime from app.controller import api_bp @@ -44,13 +44,20 @@ def validate_payload(payload): raise ValueError("'containers' key is missing.") if not isinstance(payload['containers'], list) or len(payload['containers']) == 0: raise ValueError("'containers' must be a list with at least one container.") - + # Initialize the sanitized payload sanitized_payload = { 'vdc': payload['vdc'], 'containers': [] } - + + #Confirm if the use_dns flag was set on the request at pod lvel or not + if 'use_dns' in payload: + sanitized_payload['use_dns']=False + if str(payload['use_dns']).lower=="true" or payload['use_dns']==True: + sanitized_payload['use_dns']=True + + # Validate each container in the 'containers' list and build the sanitized version for container in payload['containers']: if not isinstance(container, dict): @@ -118,6 +125,7 @@ def validate_payload(payload): else: raise ValueError("'networks' must be a string or a list of strings.") + # Add the sanitized container to the sanitized payload sanitized_payload['containers'].append(sanitized_container) @@ -148,6 +156,7 @@ def add_container_workload(): # Validate incoming data validated_data = validate_payload(request_data) + logger.debug(validated_data) request_vdc = VirtualDataCenter.query.filter_by(id=uuid.UUID(validated_data['vdc'])).first() if request_vdc is None: @@ -171,7 +180,9 @@ def add_container_workload(): # --- Step 1: Create a new Pod --- new_pod = ContainerPod( name=f"Pod_{uuid.uuid4()}", - workload_host_id=selected_host.id + workload_host_id=selected_host.id, + vdc_id=request_vdc.id + ) db.session.add(new_pod) db.session.flush() # Flush so we get new_pod.id @@ -249,18 +260,6 @@ def add_container_workload(): external_port=assigned_external_port ) db.session.add(new_port_mapping) - api_token = "ri6lIjM-aRJBY_xZ82w0Haew93U6YgZYHi5jby1-" - account_id = "5095a74b62fee53cc5d997c67443bac5" - zone_id = "e2cafdd8929869d5db885d8824f514b5" - logger.info("Adding Cloudflare") - cloudflare_manager = CloudflareTunnelManager(api_token, account_id, zone_id) - info = cloudflare_manager.setup_tunnel( - tunnel_name=str(new_container.id), - dns_hostname=f"{new_container.id}.hawkvelt.tech", - local_ip_port=f"127.0.0.1:{_container['ports'][0]['internal']}") - cloudflare_tunnel_token=info['token'] - - client_response.append({ "pod_id": str(new_pod.id), @@ -269,9 +268,31 @@ def add_container_workload(): "container_status": "pending-allocation", }) + if validated_data['use_dns']: + logger.debug("use_dns set to true, building Cloudflare tunnel") + + api_token = "ri6lIjM-aRJBY_xZ82w0Haew93U6YgZYHi5jby1-" + account_id = "5095a74b62fee53cc5d997c67443bac5" + zone_id = "e2cafdd8929869d5db885d8824f514b5" + logger.info("Adding Cloudflare") + cloudflare_manager = CloudflareTunnelManager(api_token, account_id, zone_id, logger) + info = cloudflare_manager.setup_tunnel( + tunnel_name=f"tun_{new_container.id}", + dns_hostname=f"{new_container.id}.hawkvelt.tech", + local_ip_port=f"127.0.0.1:{_container['ports'][0]['internal']}") + cloudflare_tunnel_token=info['token'] + new_cloudflare_tunnel=CloudflareTunnel() + new_cloudflare_tunnel.token=cloudflare_tunnel_token + new_cloudflare_tunnel.account_id=account_id + new_cloudflare_tunnel.tunnel_id=info['tunnel_id'] + new_cloudflare_tunnel.name=info['tunnel_name'] + new_cloudflare_tunnel.tunnel_secret=info['tunnel_secret'] + new_cloudflare_tunnel.nscontroller_workload_id=new_nscontroller.id + db.session.add(new_cloudflare_tunnel) + db.session.flush() + logger.info("Adding Cloudflare complete") + - # --- Step 4b: If Cloudflare tunnel token was generated, create sidecar --- - if cloudflare_tunnel_token: sidecar_container_name = f"cloudflared-sidecar-{new_container.id}" sidecar_launch_params = { @@ -306,6 +327,8 @@ def add_container_workload(): "container_name": sidecar_workload.name, "container_status": "pending-allocation", }) + else: + logger.debug("use_dns not True, skipping Cloudflare") # --- Step 5: Update NSController launch params with final ports --- new_nscontroller.launch_params = json.dumps(nscontroller_launch_params) @@ -554,3 +577,89 @@ def delete_container_workload(workload_id): def get_container_workloads(): workloads = Workload.query.filter(or_(Workload.workload_type == "Container", Workload.workload_type == "NSController"), Workload.deleted==False).all() return jsonify([workload.to_json() for workload in workloads]) + +@api_bp.route('/workloads/pods', methods=['GET']) +def get_pods(): + pods = ContainerPod.query.filter_by(deleted=False).all() + response = [] + + for pod in pods: + + response.append({ + "pod_id": str(pod.id), + "pod_name": pod.name, + "workload_host_id": str(pod.workload_host_id), + "vdc_id": str(pod.vdc_id), + "region_id": str(pod.vdc.region_id) if pod.vdc else None, + "nscontroller_workload_id": str(pod.nscontroller_workload_id), + "containers": [ + { + "container_id": str(mapping.container.id), + "container_name": mapping.container.name, + "status": mapping.container.status, + "launch_params": json.loads(mapping.container.launch_params or "{}") + } + for mapping in pod.container_mappings + ] + }) + + return jsonify(response), 200 + +@api_bp.route('/workloads/pods/', methods=['GET']) +def get_pod(pod_id): + try: + pod_uuid = uuid.UUID(pod_id) + except ValueError: + return jsonify({"error": "Invalid pod ID"}), 404 + + pod = ContainerPod.query.filter_by(id=pod_uuid, deleted=False).first_or_404() + + response = { + "pod_id": str(pod.id), + "pod_name": pod.name, + "workload_host_id": str(pod.workload_host_id), + "vdc_id": str(pod.vdc_id), + "region_id": str(pod.vdc.region_id) if pod.vdc else None, + "nscontroller_workload_id": str(pod.nscontroller_workload_id), + "containers": [ + { + "container_id": str(mapping.container.id), + "container_name": mapping.container.name, + "status": mapping.container.status, + "launch_params": json.loads(mapping.container.launch_params or "{}") + } + for mapping in pod.container_mappings + ] + } + + return jsonify(response), 200 + +@api_bp.route('/workloads/pods/', methods=['DELETE']) +def delete_pod(pod_id): + try: + pod_uuid = uuid.UUID(pod_id) + except ValueError: + return jsonify({"error": "Invalid pod ID"}), 404 + + pod = ContainerPod.query.filter_by(id=pod_uuid, deleted=False).first_or_404() + + # Cleanup host port mappings + for mapping in pod.container_mappings: + port_mappings = WorkloadHostPortMapping.query.filter_by(container_workload_id=mapping.container.id).all() + for pm in port_mappings: + db.session.delete(pm) + + # Set desired state on the pod + pod.status = "pending-deleted" + db.session.add(pod) + + # Also optionally mark all containers inside as "pending-deleted" (for tracking) + for mapping in pod.container_mappings: + container = mapping.container + container.set_status("pending-deleted") + db.session.add(container) + + db.session.commit() + + logger.info(f"Marked pod {pod.id} and its containers as pending-deleted") + return jsonify({'message': 'Pod and containers set to pending-deleted'}), 200 diff --git a/app/models/models.py b/app/models/models.py index 707774f..95a50b6 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -419,17 +419,17 @@ class ContainerPodContainer(db.Model): pod = relationship("ContainerPod", back_populates="container_mappings") container = relationship("Workload", back_populates="pod_mappings") - class ContainerPod(BaseModel): __tablename__ = "container_pods" workload_host_id = Column(UUID(as_uuid=True), ForeignKey("workload_hosts.id")) nscontroller_workload_id = Column(UUID(as_uuid=True), ForeignKey("workloads.id")) + vdc_id = Column(UUID(as_uuid=True), ForeignKey("vdcs.id"), nullable=False) # <-- ADD THIS workload_host = relationship("WorkloadHost", backref="container_pods") nscontroller_workload = relationship("Workload", foreign_keys=[nscontroller_workload_id]) - container_mappings = relationship("ContainerPodContainer", back_populates="pod", cascade="all, delete-orphan") + vdc = relationship("VirtualDataCenter") # optional but nice for easy joins class Image(BaseModel): __tablename__ = "images" @@ -501,3 +501,40 @@ class User(db.Model): } +class CloudflareTunnel(BaseModel): + __tablename__ = "cloudflare_tunnels" + + account_id = Column(String, nullable=False) + tunnel_id = Column(String, nullable=False, unique=True) + tunnel_secret = Column(String, nullable=False) + token = Column(String, nullable=False) + associated_hostname = Column(String, nullable=True) + notes = Column(Text, nullable=True) + nscontroller_workload_id = Column(UUID(as_uuid=True), ForeignKey("workloads.id")) + + def to_json(self): + data = super().to_json() + # Optionally hide the secret/token from normal to_json if you want + data.pop("tunnel_secret", None) + data.pop("token", None) + return data + + def to_sensitive_json(self): + """Return full JSON including secrets""" + return super().to_json() + + +class CloudflareDNSRecord(BaseModel): + __tablename__ = "cloudflare_dns_records" + + zone_id = Column(String, nullable=False) + dns_record_id = Column(String, nullable=False, unique=True) + hostname = Column(String, nullable=False) + record_type = Column(String, default="CNAME", nullable=False) + content = Column(String, nullable=False) + ttl = Column(Integer, default=120) + proxied = Column(Boolean, default=True) + notes = Column(Text, nullable=True) + + def to_json(self): + return super().to_json() diff --git a/app/services/cloudflare.py b/app/services/cloudflare.py index 5c795da..ad6ea35 100644 --- a/app/services/cloudflare.py +++ b/app/services/cloudflare.py @@ -5,13 +5,8 @@ import secrets import logging CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4" - -# Setup logger -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -logger = logging.getLogger(__name__) - class CloudflareTunnelManager: - def __init__(self, api_token: str, account_id: str, zone_id: str): + def __init__(self, api_token: str, account_id: str, zone_id: str, logger): self.api_token = api_token self.account_id = account_id self.zone_id = zone_id @@ -19,10 +14,11 @@ class CloudflareTunnelManager: "Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json" } + self.logger=logger def create_tunnel(self, tunnel_name: str) -> dict: """Create a new tunnel or get existing tunnel info.""" - logger.info(f"Checking if tunnel '{tunnel_name}' already exists...") + self.logger.info(f"Checking if tunnel '{tunnel_name}' already exists...") try: tunnels = requests.get( @@ -31,16 +27,16 @@ class CloudflareTunnelManager: ) tunnels.raise_for_status() except Exception as e: - logger.error(f"Failed to fetch tunnels list: {e}") + self.logger.error(f"Failed to fetch tunnels list: {e}") raise for tunnel in tunnels.json().get('result', []): if tunnel['name'] == tunnel_name: - logger.info(f"Tunnel '{tunnel_name}' already exists (ID: {tunnel['id']})") - logger.info(tunnel) + self.logger.info(f"Tunnel '{tunnel_name}' already exists (ID: {tunnel['id']})") + self.logger.info(tunnel) return tunnel - logger.info(f"Tunnel '{tunnel_name}' not found. Creating new tunnel...") + self.logger.info(f"Tunnel '{tunnel_name}' not found. Creating new tunnel...") payload = { "name": tunnel_name, "tunnel_secret": secrets.token_hex(32) @@ -52,16 +48,16 @@ class CloudflareTunnelManager: json=payload ) response.raise_for_status() - logger.info(f"Tunnel '{tunnel_name}' created successfully.") - logger.info(response.json()) + self.logger.info(f"Tunnel '{tunnel_name}' created successfully.") + self.logger.info(response.json()) return response.json()['result'] except Exception as e: - logger.error(f"Failed to create tunnel '{tunnel_name}': {e}") + self.logger.error(f"Failed to create tunnel '{tunnel_name}': {e}") raise def configure_tunnel_route(self, tunnel_id: str, hostname: str, service_url: str): """Configure the tunnel's public hostname routing.""" - logger.info(f"Configuring tunnel route (corrected payload): {hostname} -> {service_url}") + self.logger.info(f"Configuring tunnel route (corrected payload): {hostname} -> {service_url}") payload = { "config": { "ingress": [ @@ -83,15 +79,15 @@ class CloudflareTunnelManager: json=payload ) response.raise_for_status() - logger.info(f"Tunnel route configured successfully for hostname '{hostname}'.") + self.logger.info(f"Tunnel route configured successfully for hostname '{hostname}'.") return response.json() except Exception as e: - logger.error(f"Failed to configure tunnel route for '{hostname}': {e}") + self.logger.error(f"Failed to configure tunnel route for '{hostname}': {e}") raise def create_dns_record(self, hostname: str, tunnel_id: str): """Create DNS CNAME record pointing to the tunnel.""" - logger.info(f"Creating DNS record for hostname '{hostname}'...") + self.logger.info(f"Creating DNS record for hostname '{hostname}'...") payload = { "type": "CNAME", "name": hostname, @@ -106,29 +102,81 @@ class CloudflareTunnelManager: json=payload ) response.raise_for_status() - logger.info(f"DNS record for '{hostname}' created successfully.") + self.logger.info(f"DNS record for '{hostname}' created successfully.") return response.json() except Exception as e: - logger.error(f"Failed to create DNS record for '{hostname}': {e}") + self.logger.error(f"Failed to create DNS record for '{hostname}': {e}") raise def setup_tunnel(self, tunnel_name: str, dns_hostname: str, local_ip_port: str): """Full workflow: Create tunnel, configure, and create DNS.""" - logger.info(f"Starting full tunnel setup: {tunnel_name} -> {dns_hostname} -> {local_ip_port}") + self.logger.info(f"Starting full tunnel setup: {tunnel_name} -> {dns_hostname} -> {local_ip_port}") try: tunnel = self.create_tunnel(tunnel_name) self.configure_tunnel_route(tunnel['id'], dns_hostname, local_ip_port) self.create_dns_record(dns_hostname,tunnel['id']) token=tunnel['token'] - logger.info(f"Tunnel setup completed successfully.") + self.logger.info(f"Tunnel setup completed successfully.") return { "tunnel_id": tunnel['id'], "tunnel_name": tunnel_name, + "tunnel_secret": tunnel['credentials_file']['TunnelSecret'], "token": token, "dns_hostname": dns_hostname, "local_service": local_ip_port } except Exception as e: - logger.error(f"Full tunnel setup failed: {e}") + self.logger.error(f"Full tunnel setup failed: {e}") raise + + def delete_tunnel(self, tunnel_id: str): + """Delete a Cloudflare Tunnel.""" + self.logger.info(f"Attempting to delete tunnel ID '{tunnel_id}'...") + try: + response = requests.delete( + f"{CLOUDFLARE_API_BASE}/accounts/{self.account_id}/cfd_tunnel/{tunnel_id}", + headers=self.headers + ) + response.raise_for_status() + self.logger.info(f"Tunnel ID '{tunnel_id}' deleted successfully.") + return response.json() + except Exception as e: + self.logger.error(f"Failed to delete tunnel '{tunnel_id}': {e}") + raise + + def delete_dns_record(self, dns_record_id: str): + """Delete a DNS record.""" + self.logger.info(f"Attempting to delete DNS record ID '{dns_record_id}'...") + try: + response = requests.delete( + f"{CLOUDFLARE_API_BASE}/zones/{self.zone_id}/dns_records/{dns_record_id}", + headers=self.headers + ) + response.raise_for_status() + self.logger.info(f"DNS record ID '{dns_record_id}' deleted successfully.") + return response.json() + except Exception as e: + self.logger.error(f"Failed to delete DNS record '{dns_record_id}': {e}") + raise + + def get_dns_record_id(self, hostname: str): + """Helper: Find DNS record ID for a given hostname.""" + self.logger.info(f"Fetching DNS record for hostname '{hostname}'...") + try: + response = requests.get( + f"{CLOUDFLARE_API_BASE}/zones/{self.zone_id}/dns_records?type=CNAME&name={hostname}", + headers=self.headers + ) + response.raise_for_status() + result = response.json().get('result', []) + if result: + dns_record_id = result[0]['id'] + self.logger.info(f"Found DNS record ID '{dns_record_id}' for hostname '{hostname}'.") + return dns_record_id + else: + self.logger.warning(f"No DNS record found for hostname '{hostname}'.") + return None + except Exception as e: + self.logger.error(f"Failed to fetch DNS record for '{hostname}': {e}") + raise \ No newline at end of file diff --git a/streamlit_server/views/containers.py b/streamlit_server/views/containers.py index 430140d..0aad7a3 100644 --- a/streamlit_server/views/containers.py +++ b/streamlit_server/views/containers.py @@ -2,35 +2,28 @@ import json import streamlit as st from streamlit_server.utils.helpers import format_timestamp - def render_list(): """ - Renders a list of all container workloads. + Renders a list of container pods, each with its containers. """ st.header("Container Workload Management") - # Initialize session state variables for delete all confirmation - if 'container_delete_all_confirmation_shown' not in st.session_state: - st.session_state.container_delete_all_confirmation_shown = False - if 'container_delete_all_confirmed' not in st.session_state: - st.session_state.container_delete_all_confirmed = False - # Initialize session state variables for delete confirmation - if 'container_delete_confirmation_shown' not in st.session_state: - st.session_state.container_delete_confirmation_shown = False - if 'container_delete_confirmed' not in st.session_state: - st.session_state.container_delete_confirmed = False + # Initialize session state flags for pod deletion + if 'pod_delete_all_confirmation_shown' not in st.session_state: + st.session_state.pod_delete_all_confirmation_shown = False + if 'pod_delete_all_confirmed' not in st.session_state: + st.session_state.pod_delete_all_confirmed = False + if 'pod_delete_confirmation_shown' not in st.session_state: + st.session_state.pod_delete_confirmation_shown = False + if 'pod_delete_confirmed' not in st.session_state: + st.session_state.pod_delete_confirmed = False - # Fetch all VDCs for the dropdown + # Fetch VDCs vdcs = st.session_state.client.get_virtual_data_centers() vdc_options = {vdc['name']: vdc['id'] for vdc in vdcs} if vdcs else {} - # Add a dropdown to select VDC selected_vdc_name = st.selectbox("Filter by Virtual Data Center", list(vdc_options.keys())) - # Fetch all Workloads_hosts for the dropdown - workload_hosts = st.session_state.client.get_workload_hosts() - workload_host_options = {workload_host['hostname']: workload_host['id'] for workload_host in workload_hosts} if workload_hosts else {} - # Create Container Workload Form with st.expander("Create New Container Workload"): with st.form("create_container"): @@ -56,16 +49,18 @@ def render_list(): internal_port = st.number_input("Internal Port", min_value=1, max_value=65535, step=1) external_port = st.number_input("External Port", min_value=1, max_value=65535, step=1) + # New: Use DNS (Cloudflare) Checkbox + use_dns = st.checkbox("Use DNS (Cloudflare)", value=True) + submit = st.form_submit_button("Create Container Workload") if submit and selected_vdc_name_create and container_name: - # Use the selected VDC's ID vdc_id = vdc_options[selected_vdc_name_create] container_payload = { "docker_image": docker_image, "container_name": container_name, - "networks": "bridge" # Default network + "networks": "bridge", # Default network } # Only add ports if both fields are non-zero @@ -74,6 +69,7 @@ def render_list(): payload = { "vdc": vdc_id, + "use_dns": use_dns, "containers": [container_payload] } @@ -82,87 +78,78 @@ def render_list(): st.success("Container workload created successfully!") + # List Container Pods + st.subheader("Existing Container Pods") + pods = st.session_state.client.get_container_pods() - # List Container Workloads - st.subheader("Existing Container Workloads") - containers = st.session_state.client.get_containers() + if pods: + # Filter by selected VDC + selected_vdc_id = vdc_options[selected_vdc_name] + filtered_pods = [pod for pod in pods if pod['vdc_id'] == selected_vdc_id] - if containers: - # Filter containers based on the selected VDC - if selected_vdc_name: - selected_vdc_id = vdc_options[selected_vdc_name] - filtered_containers = [container for container in containers if container['vdc_id'] == selected_vdc_id] - else: - filtered_containers = containers - - # Delete All functionality - if not st.session_state.container_delete_all_confirmation_shown: - if st.button("Delete All Containers", key="delete_all_containers", type="primary", help="This will delete all visible containers"): - st.session_state.container_delete_all_confirmation_shown = True + # Delete All Pods + if not st.session_state.pod_delete_all_confirmation_shown: + if st.button("Delete All Pods", key="delete_all_pods", type="primary", help="This will delete all visible pods"): + st.session_state.pod_delete_all_confirmation_shown = True st.rerun() - - # Show confirmation if delete was clicked - if st.session_state.container_delete_all_confirmation_shown and not st.session_state.container_delete_all_confirmed: - st.warning("Are you sure you want to delete all containers? This action cannot be undone.") + + if st.session_state.pod_delete_all_confirmation_shown and not st.session_state.pod_delete_all_confirmed: + st.warning("Are you sure you want to delete all pods? This action cannot be undone.") confirm_col1, confirm_col2 = st.columns([1, 1]) with confirm_col1: - if st.button("Yes, Delete All", key="confirm_delete_all_containers"): - st.session_state.container_delete_all_confirmed = True + if st.button("Yes, Delete All Pods", key="confirm_delete_all_pods"): + st.session_state.pod_delete_all_confirmed = True st.rerun() with confirm_col2: - if st.button("Cancel", key="cancel_delete_all_containers"): - st.session_state.container_delete_all_confirmation_shown = False + if st.button("Cancel", key="cancel_delete_all_pods"): + st.session_state.pod_delete_all_confirmation_shown = False st.rerun() - - # Perform deletion if confirmed - if st.session_state.container_delete_all_confirmed: + + if st.session_state.pod_delete_all_confirmed: deletion_count = 0 - for container in filtered_containers: - # Call the delete API for each container - result = st.session_state.client.delete_container(container['id']) + for pod in filtered_pods: + result = st.session_state.client.delete_container_pod(pod['pod_id']) if result: deletion_count += 1 - - st.success(f"Successfully deleted {deletion_count} containers.") - # Reset the confirmation flags - st.session_state.container_delete_all_confirmation_shown = False - st.session_state.container_delete_all_confirmed = False - - # Display the containers - for container in filtered_containers: - with st.container(): - col1, col2, col3 = st.columns([3, 1, 1]) - with col1: - # Safely handle empty or None launch_params - launch_params = container.get('launch_params') # Use .get() to avoid KeyError if 'launch_params' doesn't exist - if launch_params: # Check if launch_params is not empty or None - try: - container_name = json.loads(launch_params).get('docker_image') # Use .get() to avoid KeyError if 'docker_image' doesn't exist - except json.JSONDecodeError: - print("LP not vald json") - # Handle the case where launch_params is not valid JSON - container_name = None - else: - print("lp empty") - # Handle the case where launch_params is empty or None - container_name = None - st.markdown(f"### {container['name']}") - st.text(f"Status: {container['status']}") - st.text(f"VDC ID: {container['vdc_id']}") - st.text(f"Image: {container_name}") - with col2: - st.text(f"ID: {container['id']}") - with col3: - if st.button("View Details", key=f"view_{container['id']}"): - st.session_state.selected_resource = container - st.session_state.view_type = 'detail' - st.session_state.selected_resource_type = 'container' + st.success(f"Successfully deleted {deletion_count} pods.") + st.session_state.pod_delete_all_confirmation_shown = False + st.session_state.pod_delete_all_confirmed = False + st.rerun() + + # Render Pods nicely + for pod in filtered_pods: + with st.expander(f"🛰️ Pod: {pod['pod_name']} (ID: {pod['pod_id']})", expanded=False): + st.markdown(f"**Virtual Data Center ID:** `{pod['vdc_id']}`") + st.markdown(f"**Region ID:** `{pod.get('region_id', 'N/A')}`") + st.markdown(f"**NSController ID:** `{pod['nscontroller_workload_id']}`") + st.markdown("---") + + # Container List + for container in pod['containers']: + with st.container(): + cols = st.columns([3, 2, 1]) + with cols[0]: + st.markdown(f"**Container Name:** {container['container_name']}") + st.text(f"Status: {container['status']}") + with cols[1]: + st.text(f"Container ID: {container['container_id']}") + with cols[2]: + if st.button("View Details", key=f"view_{container['container_id']}"): + st.session_state.selected_resource = container + st.session_state.view_type = 'detail' + st.session_state.selected_resource_type = 'container' + st.rerun() + st.markdown("---") + + # Delete pod button + if st.button(f"🗑️ Delete Pod {pod['pod_name']}", key=f"delete_pod_{pod['pod_id']}"): + result = st.session_state.client.delete_container_pod(pod['pod_id']) + if result: + st.success(f"Deleted Pod {pod['pod_name']}") st.rerun() - # st.switch_page("streamlit_server/views/containers.py&container_id=d", query_params={"team": "streamlit"}) - else: - st.info("No container workloads found.") + st.info("No pods found in the selected VDC.") def render_detail():