rebuilt container launch process to offload nscontoller into standard container creation workflow

This commit is contained in:
2025-05-04 15:54:13 +09:30
parent 72ce82c208
commit 945c0a2097
2 changed files with 186 additions and 99 deletions
+38 -15
View File
@@ -36,30 +36,53 @@ def render_list():
with st.form("create_container"):
# Dropdown for VDC selection
selected_vdc_name_create = st.selectbox("VDC", list(vdc_options.keys()))
container_name = st.text_input("Container Name",value="my-container")
docker_image = st.text_input("Docker Image", value="nginx")
container_name = st.text_input("Container Name", value="my-container")
# Suggestions for common images
docker_image_options = ["nginx", "traefik/whoami:latest", "whoami81"]
docker_image = st.selectbox(
"Docker Image",
options=docker_image_options + ["Custom..."],
index=0
)
# If 'Custom...' selected, let user input their own image
if docker_image == "Custom...":
docker_image = st.text_input("Custom Docker Image", value="")
# Port mapping (single)
st.markdown("**Optional: Port Mapping**")
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)
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]
result = st.session_state.client.create_container({
container_payload = {
"docker_image": docker_image,
"container_name": container_name,
"networks": "bridge" # Default network
}
# Only add ports if both fields are non-zero
if internal_port and external_port:
container_payload["ports"] = [{"internal": int(internal_port), "external": int(external_port)}]
payload = {
"vdc": vdc_id,
"containers": [
{
"docker_image": docker_image,
"container_name": container_name,
"ports": [
{"internal": 80, "external": 8080},
{"internal": 443, "external": 8443}
],
"networks": "bridge"
}
]
})
"containers": [container_payload]
}
result = st.session_state.client.create_container(payload)
if result:
st.success("Container workload created successfully!")
# List Container Workloads
st.subheader("Existing Container Workloads")
containers = st.session_state.client.get_containers()
+148 -84
View File
@@ -1,7 +1,7 @@
import docker
import json
from logging import Logger
import time
class ContainerTask:
def __init__(self, params, logger):
@@ -96,15 +96,18 @@ class ContainerTask:
else:
self.logger.info(f"Container '{container_name}' does not exist. No action needed.")
def launch_container(self, client, container):
"""Launch the container as specified in the container variable, attaching it to the namespace controller."""
"""Launch the container as specified in the container variable, attaching it to the namespace controller if needed."""
container_name = container['container_id']
real_container_name=container['container_name']
self.logger.warn(container)
self.logger.warn(f"Inside launch_container for {real_container_name}")
# Check if the container is marked for deletion
if container.get('desired_state', False):
self.logger.info(f"Container '{container_name}' is marked for deletion. Ensuring it is not present...")
self.delete_container(client, container_name)
return # Exit the function as no further action is needed for deleted containers
return
self.logger.info(f"Checking if container '{container_name}' already exists...")
running_containers = client.containers.list(all=True, filters={"name": container_name})
@@ -112,120 +115,156 @@ class ContainerTask:
if running_containers:
existing_container = running_containers[0]
self.logger.info(f"Container '{container_name}' exists. Checking configuration and status...")
# Check if the container is running
if existing_container.status != 'running':
self.logger.info(f"Container '{container_name}' is not running. Triggering rebuild...")
self.delete_container(client, container_name)
# Proceed to launch a new container
else:
# Normalize image names for comparison
payload_image = container['docker_image']
if ':' not in payload_image:
payload_image += ':latest' # Assume 'latest' if no tag is specified
payload_image += ':latest'
container_image = existing_container.image.tags[0]
if ':' not in container_image:
container_image += ':latest' # Assume 'latest' if no tag is specified
# Check if the existing container matches the payload configuration
# TODO - We are only matchin on CPU, RAM and image here, we should be more detailed with the config comparison
cpu_matches = existing_container.attrs['HostConfig']['CpuShares'] == container['cpu_shares'] * 1024
memory_matches = existing_container.attrs['HostConfig']['Memory'] == container['mem_limit'] * 1024 * 1024
container_image += ':latest'
# Compare CPU, RAM, Image
cpu_matches = existing_container.attrs['HostConfig']['CpuShares'] == container.get('cpu_shares', 0) * 1024
memory_matches = existing_container.attrs['HostConfig']['Memory'] == container.get('mem_limit', 0) * 1024 * 1024
image_matches = container_image == payload_image
if cpu_matches and memory_matches and image_matches:
# Compare Ports
ports_match = True
expected_ports = {}
if 'ports' in container:
for mapping in container['ports']:
internal = mapping.get('internal')
external = mapping.get('external')
if internal and external:
expected_ports[str(internal) + '/tcp'] = str(external)
container_ports = existing_container.attrs['HostConfig'].get('PortBindings', {})
for port_proto, expected_external in expected_ports.items():
existing_binding = container_ports.get(port_proto)
if not existing_binding or str(existing_binding[0]['HostPort']) != expected_external:
ports_match = False
break
# Compare Networks
networks_match = True
expected_networks = container.get('networks')
attached_networks = list(existing_container.attrs['NetworkSettings']['Networks'].keys())
if expected_networks:
if isinstance(expected_networks, str):
expected_networks = [expected_networks]
if sorted(expected_networks) != sorted(attached_networks):
networks_match = False
# Final match check
if cpu_matches and memory_matches and image_matches and ports_match and networks_match:
self.logger.info(f"Container '{container_name}' already exists with the same configuration and is running. No action needed.")
return
else:
self.logger.info(f"Container '{container_name}' exists but with a different configuration. Details of the mismatch:")
self.logger.info(f"Container '{container_name}' exists but with different configuration. Details of mismatch:")
if not cpu_matches:
self.logger.info(f" - CPU: Existing container has {existing_container.attrs['HostConfig']['CpuShares'] // 1024} CPUs, but payload specifies {container['cpu_shares']} CPUs.")
self.logger.info(f" - CPU: Existing {existing_container.attrs['HostConfig']['CpuShares'] // 1024} vs requested {container.get('cpu_shares')}")
if not memory_matches:
self.logger.info(f" - Memory: Existing container has {existing_container.attrs['HostConfig']['Memory'] // (1024 * 1024)} MB, but payload specifies {container['mem_limit']} MB.")
self.logger.info(f" - Memory: Existing {existing_container.attrs['HostConfig']['Memory'] // (1024 * 1024)} MB vs requested {container.get('mem_limit')}")
if not image_matches:
self.logger.info(f" - Image: Existing container uses image '{container_image}', but payload specifies '{payload_image}'.")
self.logger.info(f"Removing container '{container_name}' and recreating with the new configuration...")
self.logger.info(f" - Image: Existing {container_image} vs requested {payload_image}")
if not ports_match:
self.logger.info(f" - Ports: Existing {container_ports} vs expected {expected_ports}")
if not networks_match:
self.logger.info(f" - Networks: Existing {attached_networks} vs expected {expected_networks}")
self.logger.info(f"Removing container '{container_name}' and recreating...")
self.delete_container(client, container_name)
#Check if the NSController container exists
# Check if the NSController container exists
nscontroller_container_name = container['NSController_launchparams']['container_name']
self.logger.debug(f"Name of NSController is {nscontroller_container_name}")
running_NSController = client.containers.list(all=True, filters={"name": nscontroller_container_name})
self.logger.debug(f"{running_NSController}")
if not running_NSController:
self.logger.info("NSController does not exist, creating it")
# Handle NSController launching if specified
if 'NSController_launchparams' in container:
ns_params = container['NSController_launchparams']
nscontroller_container_name = ns_params['container_name']
nscontroller_container_config = {
"image": ns_params['docker_image'],
"name": ns_params['container_name'],
"command": ns_params.get('command'), # safer with .get()
"dns": ["1.1.1.1"],
"detach": True,
self.logger.debug(f"Name of NSController is {nscontroller_container_name}")
# running_NSController = client.containers.list(all=True, filters={"name": nscontroller_container_name})
# if not running_NSController:
self.logger.info(f"NSController '{nscontroller_container_name}' does not exist, creating it")
nscontroller_container_spec = {
"container_id": ns_params['container_id'],
"container_name": ns_params['container_name'],
"docker_image": ns_params['docker_image'],
"command": ns_params.get('command'),
"labels": {
"managed_by": "worker_agent",
"system_container_id": ns_params['container_id']
}
}
# Optional: Attach network
primary_network = None
additional_networks = []
if 'networks' in ns_params:
network_param = ns_params['networks']
if isinstance(network_param, str):
primary_network = network_param
elif isinstance(network_param, list) and network_param:
primary_network = network_param[0]
additional_networks = network_param[1:]
else:
raise ValueError("Invalid 'network' field: must be string or non-empty list of strings.")
nscontroller_container_config["network"] = primary_network
# Only allow port mapping if the primary network is "bridge"
if primary_network == 'bridge' and 'ports' in ns_params:
ports_mapping = {}
for mapping in ns_params['ports']:
internal = mapping.get('internal')
external = mapping.get('external')
if internal and external:
ports_mapping[internal] = external
if ports_mapping:
nscontroller_container_config["ports"] = ports_mapping
# Launch the NSController container
ns_container = client.containers.run(**nscontroller_container_config)
# After launch: connect to any additional networks
for net_name in additional_networks:
client.networks.get(net_name).connect(ns_container)
nscontroller_container_spec['networks'] = ns_params['networks']
if 'ports' in ns_params:
nscontroller_container_spec['ports'] = ns_params['ports']
if 'volumes' in ns_params:
nscontroller_container_spec['volumes'] = ns_params['volumes']
if 'environment' in ns_params:
nscontroller_container_spec['environment'] = ns_params['environment']
self.launch_container(client, nscontroller_container_spec)
self.logger.info(f"NSController container '{nscontroller_container_name}' launched successfully.")
# Prepare new container config
self.logger.info(f"Launching container '{container_name}'...")
# Prepare container configuration
container_config = {
"image": container['docker_image'],
"name": container_name,
"network": f"container:{nscontroller_container_name}",
"detach": True,
"labels": {
"managed_by": "worker_agent",
"system_container_id": container['container_id']
} # Add a label to identify containers started by this worker
}
}
# Add optional fields if they exist in the container
additional_networks = []
ports_mapping = {}
# Handle networks
primary_network = None
if 'networks' in container:
network_param = container['networks']
if isinstance(network_param, str):
primary_network = network_param
elif isinstance(network_param, list) and network_param:
primary_network = network_param[0]
additional_networks = network_param[1:]
else:
raise ValueError("Invalid 'networks' field: must be a string or non-empty list.")
if primary_network:
container_config['network'] = primary_network
# Handle ports if bridge
if primary_network == 'bridge' and 'ports' in container:
for mapping in container['ports']:
internal = mapping.get('internal')
external = mapping.get('external')
if internal and external:
ports_mapping[internal] = external
if ports_mapping:
container_config['ports'] = ports_mapping
# If attached to NSController, override network mode
if 'NSController_launchparams' in container:
container_config['network'] = f"container:{container['NSController_launchparams']['container_id']}"
# container_config['network'] = f"container:{container['NSController_launchparams']['container_name']}"
# Optional fields
if "command" in container:
container_config["command"] = container['command']
if "environment" in container:
@@ -235,7 +274,7 @@ class ContainerTask:
if "restart_policy" in container:
container_config["restart_policy"] = container['restart_policy']
if "labels" in container:
container_config["labels"] = container['labels']
container_config["labels"].update(container['labels'])
if "user" in container:
container_config["user"] = container['user']
if "working_dir" in container:
@@ -245,9 +284,34 @@ class ContainerTask:
if "mem_limit" in container:
container_config["mem_limit"] = f"{container['mem_limit']}m"
# Launch the container
client.containers.run(**container_config)
self.logger.info(f"Container '{container_name}' launched successfully.")
# Remove None fields
container_config = {k: v for k, v in container_config.items() if v is not None}
# Launch container with retries
max_retries = 3
retry_delay = 2 # seconds
launched_container = None
for attempt in range(1, max_retries + 1):
try:
launched_container = client.containers.run(**container_config)
self.logger.info(f"Container '{container_name}' launched successfully on attempt {attempt}.")
break
except (docker.errors.APIError, docker.errors.DockerException, Exception) as e:
self.logger.error(f"Attempt {attempt} to launch container '{container_name}' failed: {str(e)}")
if attempt == max_retries:
self.logger.error(f"All {max_retries} attempts to launch container '{container_name}' failed. Raising exception.")
raise
else:
self.logger.info(f"Retrying launch of container '{container_name}' in {retry_delay} seconds...")
time.sleep(retry_delay)
# Connect additional networks after launch
if additional_networks:
for net_name in additional_networks:
self.logger.info(f"Connecting container '{container_name}' to additional network '{net_name}'...")
client.networks.get(net_name).connect(launched_container)
def Create(self):
self.logger.info("Executing ContainerTask - Create...")