rebuilt container launch process to offload nscontoller into standard container creation workflow
This commit is contained in:
@@ -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
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user