uplifted the pod concept

added a use_dns flag at container creation time
This commit is contained in:
2025-05-05 16:29:48 +09:30
parent 840e691da0
commit dd36416a5e
5 changed files with 327 additions and 131 deletions
+16 -1
View File
@@ -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)
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}')
+127 -18
View File
@@ -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/<pod_id>', 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/<pod_id>', 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
+39 -2
View File
@@ -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()
+71 -23
View File
@@ -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
+74 -87
View File
@@ -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():