PortForwarding work

Added workload id to portforward object
Updated streamlit to show workload name
This commit is contained in:
2025-07-25 15:38:52 +09:30
parent 8c3b922e7a
commit ff989be202
6 changed files with 119 additions and 49 deletions
+80 -45
View File
@@ -1,6 +1,4 @@
#app/controller/api/workload_container_routes.py
import random
import time
from flask import json, request, jsonify, abort
import requests
from app import app, db, logger
@@ -8,9 +6,6 @@ from app.models.models import PortForwarding, Workload, WorkloadHost, WorkloadHo
from app.models.network import Network, NetworkPort
from datetime import datetime
from app.controller import api_bp
import uuid
from app.scheduling_filters import ExcludeAllDisabledHosts, MostAvailableCapacity, ExcludeAllOfflineHosts, DockerCapableHosts, ExcludeHostsWithoutNorthSouthIP
from app.services.cloudflare import CloudflareTunnelManager
from werkzeug.exceptions import abort
from sqlalchemy import or_
from app.utils.standard_responses import api_response
@@ -18,40 +13,55 @@ from app.utils.container_deleted import check_deleted_container
def validate_payload(payload):
"""
Validate the input payload according to the specified rules and return a sanitized version.
Validate the input payload for container deployment.
Exactly one of 'virtual_data_center' or 'pod' must be provided.
Args:
payload (dict): The input JSON object to validate.
Returns:
dict: A sanitized version of the input payload containing only the expected parameters.
Raises:
ValueError: If the payload fails any validation rule.
"""
# Ensure the input is a valid JSON object (dict)
import uuid
if not isinstance(payload, dict):
raise ValueError("Input must be a valid JSON object (dict).")
# Check for the top-level 'virtual_data_center' key and validate it as a UUID
if 'virtual_data_center' not in payload:
raise ValueError("Top-level 'virtual_data_center' key is missing.")
try:
uuid.UUID(payload['virtual_data_center'], version=4)
except ValueError:
raise ValueError("'virtual_data_center' must be a valid UUID.")
# Check for the 'containers' key and ensure it is a list with at least one container
sanitized_payload = {}
# Enforce exactly one of 'virtual_data_center' or 'pod'
has_vdc = 'virtual_data_center' in payload
has_pod = 'pod' in payload
if not (has_vdc or has_pod):
raise ValueError("Either 'virtual_data_center' or 'pod' must be provided.")
if has_vdc and has_pod:
raise ValueError("Only one of 'virtual_data_center' or 'pod' can be provided — not both.")
if has_vdc:
try:
uuid.UUID(payload['virtual_data_center'], version=4)
sanitized_payload['virtual_data_center'] = payload['virtual_data_center']
except ValueError:
raise ValueError("'virtual_data_center' must be a valid UUID.")
if has_pod:
try:
uuid.UUID(payload['pod'], version=4)
sanitized_payload['pod'] = payload['pod']
except ValueError:
raise ValueError("'pod' must be a valid UUID.")
# Validate container list
if 'containers' not in 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 = {
'virtual_data_center': payload['virtual_data_center'],
'containers': []
}
sanitized_payload['containers'] = []
# Validate each container in the 'containers' list and build the sanitized version
for container in payload['containers']:
@@ -100,11 +110,12 @@ def validate_payload(payload):
'internal': port_mapping['internal'],
'external': port_mapping['external']
}
if 'use_dns' in port_mapping:
port_data['use_dns'] = bool(port_mapping['use_dns'])
sanitized_ports.append(port_data)
if sanitized_ports:
sanitized_container['ports'] = sanitized_ports
@@ -129,9 +140,8 @@ def validate_payload(payload):
# Add the sanitized container to the sanitized payload
sanitized_payload['containers'].append(sanitized_container)
return sanitized_payload
return sanitized_payload
@api_bp.route("/workloads/containers", methods=["POST"])
def request_container_workload():
@@ -144,22 +154,32 @@ def request_container_workload():
logger.warning("Validation error: %s", exc)
return api_response(success=False, message=str(exc), status=400)[0]
request_vdc = VirtualDataCenter.query.filter_by(id=(validated_data['virtual_data_center'])).first()
# Resolve Virtual Data Center ID from either pod or direct input
if 'pod' in validated_data:
container_pod = ContainerPod.query.filter_by(id=validated_data['pod']).first()
if not container_pod:
logger.warning(f"Referenced pod ID not found: {validated_data['pod']}")
abort(404, description="Container Pod not found")
vdc_id = container_pod.vdc_id
else:
vdc_id = validated_data['virtual_data_center']
request_vdc = VirtualDataCenter.query.filter_by(id=vdc_id).first()
if request_vdc is None:
logger.warning(f"Requested a Virtual Data Center that does not exist {request_vdc}")
logger.warning(f"Requested a Virtual Data Center that does not exist: {vdc_id}")
abort(404, description="Virtual Data Center not found")
req = WorkloadRequest(
vdc_id=validated_data["virtual_data_center"],
vdc_id=vdc_id,
payload=validated_data,
status="pending-scheduling",
workload_type="container"
)
db.session.add(req)
db.session.commit()
logger.info("Created workload request %s – enqueuing Celery task", req.id)
from app.tasks.process_workload_request import process_workload
process_workload.delay(str(req.id))
return api_response(
@@ -168,6 +188,7 @@ def request_container_workload():
status=202,
)[0]
@api_bp.route('/workloads/containers/status_update/<system_container_id>', methods=['PUT'])
def update_container_workload(system_container_id):
data = request.json
@@ -314,7 +335,9 @@ def get_pods():
"protocol": pf.protocol,
"ip_address": pf.ip_address,
"dns_record_id": pf.dns_record_id,
"dns_record_hostname": pf.dns_record.hostname if pf.dns_record else None
"dns_record_hostname": pf.dns_record.hostname if pf.dns_record else None,
"container_id": pf.workload_id,
"container_name": pf.workload.name if pf.workload else None,
}
for pf in pod.port_forwardings
],
@@ -369,7 +392,9 @@ def get_pod(pod_id):
"internal_port": pf.internal_port,
"external_port": pf.external_port,
"protocol": pf.protocol,
"ip_address": pf.ip_address
"ip_address": pf.ip_address,
"container_id": pf.workload_id,
"container_name": pf.workload.name,
}
for pf in pod.port_forwardings
]
@@ -378,18 +403,22 @@ def get_pod(pod_id):
@api_bp.route('/workloads/containers/<workload_id>', methods=['DELETE'])
def delete_container_workload(workload_id):
"""
Handle deletion request for a container workload.
Sends the deletion request to the websocket server and marks status accordingly.
Also soft-deletes any associated PortForwarding records.
"""
try:
# Convert workload_id to UUID and ensure it's valid
workload_uuid = (workload_id)
workload_uuid = workload_id # Wrap with uuid.UUID(workload_id) if needed
except ValueError:
# Return 404 if it's not a valid UUID
return jsonify({"error": "Invalid workload ID"}), 404
# Query the workload
_container = Workload.query.filter(
Workload.id == workload_uuid,
or_(Workload.workload_type == "Container", Workload.workload_type == "NSController"),
or_(
Workload.workload_type == "Container",
Workload.workload_type == "NSController"
),
Workload.deleted == False
).first_or_404()
@@ -407,15 +436,23 @@ def delete_container_workload(workload_id):
]
}
}
logger.debug(f"Sending payload to websocket server {payload}")
headers = {"Content-Type": "application/json"}
websocket_server_response = requests.post(app.config['WEBSOCKET_SERVER_URL'], data=json.dumps(payload), headers=headers)
websocket_server_response_data = websocket_server_response.json()
if websocket_server_response.status_code == 201:
logger.info(f"Task created successfully for container {_container.id}")
_container.set_status("pending-deleted")
# Soft delete any associated port forwards
forwards = PortForwarding.query.filter_by(container_workload_id=_container.id, deleted=False).all()
if forwards:
logger.debug(f"Soft-deleting {len(forwards)} port forward(s) for container {_container.id}")
for pf in forwards:
pf.soft_delete()
db.session.add(pf)
db.session.add(_container)
db.session.commit()
else:
@@ -424,8 +461,6 @@ def delete_container_workload(workload_id):
db.session.add(_container)
db.session.commit()
# Dont soft delete here, it's only in delete-pending right now
# _container.soft_delete()
logger.info(f"Requested deletion of container {_container.id}")
return jsonify({'message': 'Container workload deleted successfully'}), 200
+2
View File
@@ -862,9 +862,11 @@ class PortForwarding(BaseModel):
protocol = Column(String(10), default="tcp", nullable=False)
ip_address = Column(String(25), nullable=False)
dns_record_id = Column(String(36), ForeignKey("cloudflare_dns_records.id"), nullable=True)
workload_id = Column(String(36), ForeignKey("workloads.id"), nullable=True)
pod = relationship("ContainerPod", backref="port_forwardings")
dns_record = relationship("CloudflareDNSRecord", backref="port_forwardings")
workload=relationship("Workload", backref="port_forwardings")
def to_json(self):
data = super().to_json()
+2 -1
View File
@@ -142,7 +142,8 @@ def add_container_workload(request_data):
internal_port=internal_port,
external_port=assigned_external_port,
protocol=port_mapping.get("protocol", "tcp"),
ip_address=selected_host.ip_address_northsouth
ip_address=selected_host.ip_address_northsouth,
workload_id=new_container.id,
)
db.session.add(new_forward)
db.session.flush() # Ensure we have new_forward.id
+31
View File
@@ -0,0 +1,31 @@
PODID=""
curl -X POST http://192.168.64.2:5000/api/workloads/containers \
-H "Content-Type: application/json" \
-d '{
"pod": "$PODID",
"containers": [
{
"docker_image": "joke_container",
"container_name": "very-important-webserver",
"ports": [
{
"internal": 8080,
"external": 8765,
"use_dns": false
}
]
},
{
"docker_image": "whoami81",
"container_name": "whoami-server",
"ports": [
{
"internal": 81,
"external": 8081,
"use_dns": false
}
]
}
]
}'
+2 -2
View File
@@ -1,7 +1,7 @@
curl -X POST http://localhost:5000/api/workloads/containers \
curl -X POST http://192.168.64.2:5000/api/workloads/containers \
-H "Content-Type: application/json" \
-d '{
"vdc": "ce11e1aa-18ca-4128-8d70-c09662f0c039",
"virtual_data_center": "b1d09477-742b-485e-87c3-5dad36dd4f9e",
"containers": [
{
"docker_image": "joke_container",
+2 -1
View File
@@ -145,10 +145,11 @@ def render_list():
st.markdown("🔁 **Port Forwarding:**")
for pf in pod['port_forwardings']:
dns_status = "🌐 DNS" if pf.get('dns_record_id') else "🚫 No DNS"
cols = st.columns([1, 3, 2])
cols = st.columns([1, 2, 2,1])
with cols[0]: st.markdown(f"**{pf['protocol'].upper()}**")
with cols[1]: st.text(f"{pf['external_port']} → {pf['internal_port']}")
with cols[2]: st.text(dns_status)
with cols[3]: st.text(pf['container_name'])
link = pf.get('dns_record_hostname') or f"{pf['ip_address']}:{pf['external_port']}"
st.page_link(f"http://{link}", label=f"http://{link}")
st.markdown("---")