2024-10-03 18:21:27 -07:00
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
import select
|
|
|
|
|
import shlex
|
2024-10-05 10:49:47 -07:00
|
|
|
import yaml
|
|
|
|
|
import json
|
2024-10-10 17:44:41 -07:00
|
|
|
import logging
|
2024-11-26 11:01:16 -08:00
|
|
|
import docker
|
|
|
|
|
|
|
|
|
|
from cli.consts import ARCHGW_DOCKER_IMAGE, ARCHGW_DOCKER_NAME
|
2024-10-10 17:44:41 -07:00
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def getLogger(name="cli"):
|
|
|
|
|
logger = logging.getLogger(name)
|
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
return logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log = getLogger(__name__)
|
2024-10-03 18:21:27 -07:00
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
2024-11-26 11:01:16 -08:00
|
|
|
def validate_schema(arch_config_file: str) -> None:
|
|
|
|
|
try:
|
|
|
|
|
client = docker.from_env()
|
|
|
|
|
# Run the container with detach=True to avoid blocking main process
|
|
|
|
|
container = client.containers.run(
|
|
|
|
|
image=ARCHGW_DOCKER_IMAGE,
|
|
|
|
|
volumes={
|
|
|
|
|
f"{arch_config_file}": {
|
|
|
|
|
"bind": "/app/arch_config.yaml",
|
|
|
|
|
"mode": "ro",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
entrypoint=["python", "config_generator.py"],
|
|
|
|
|
detach=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Wait for the container to finish and get the exit code
|
|
|
|
|
exit_code = container.wait()
|
|
|
|
|
|
|
|
|
|
# Check exit code for validation success
|
|
|
|
|
if exit_code["StatusCode"] != 0:
|
|
|
|
|
# Validation failed (non-zero exit code)
|
|
|
|
|
logs = container.logs().decode() # Get container logs for debugging
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Validation failed. Container exited with code {exit_code}.\nLogs:\n{logs}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Successful validation (exit code 0)
|
|
|
|
|
log.info("Schema validation successful!")
|
|
|
|
|
|
|
|
|
|
except docker.errors.APIError as e:
|
|
|
|
|
# Handle container creation error
|
|
|
|
|
raise ValueError(f"Failed to create container: {e}")
|
|
|
|
|
|
|
|
|
|
|
2024-10-03 18:21:27 -07:00
|
|
|
def run_docker_compose_ps(compose_file, env):
|
|
|
|
|
"""
|
|
|
|
|
Check if all Docker Compose services are in a healthy state.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
path (str): The path where the docker-compose.yml file is located.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2024-10-18 12:09:45 -07:00
|
|
|
# Run `docker compose ps` to get the health status of each service.
|
|
|
|
|
# This should be a non-blocking call so using subprocess.Popen(...)
|
2024-10-03 18:21:27 -07:00
|
|
|
ps_process = subprocess.Popen(
|
2024-10-09 11:25:07 -07:00
|
|
|
[
|
|
|
|
|
"docker",
|
|
|
|
|
"compose",
|
|
|
|
|
"-p",
|
|
|
|
|
"arch",
|
|
|
|
|
"ps",
|
|
|
|
|
"--format",
|
|
|
|
|
"table{{.Service}}\t{{.State}}\t{{.Ports}}",
|
|
|
|
|
],
|
2024-10-03 18:21:27 -07:00
|
|
|
cwd=os.path.dirname(compose_file),
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
text=True,
|
2024-10-08 12:40:24 -07:00
|
|
|
start_new_session=True,
|
2024-10-09 11:25:07 -07:00
|
|
|
env=env,
|
2024-10-03 18:21:27 -07:00
|
|
|
)
|
|
|
|
|
# Capture the output of `docker-compose ps`
|
|
|
|
|
services_status, error_output = ps_process.communicate()
|
|
|
|
|
|
|
|
|
|
# Check if there is any error output
|
|
|
|
|
if error_output:
|
2024-10-10 17:44:41 -07:00
|
|
|
log.info(
|
2024-10-09 11:25:07 -07:00
|
|
|
f"Error while checking service status:\n{error_output}",
|
|
|
|
|
file=os.sys.stderr,
|
|
|
|
|
)
|
2024-10-03 18:21:27 -07:00
|
|
|
return {}
|
|
|
|
|
|
2024-10-05 10:49:47 -07:00
|
|
|
services = parse_docker_compose_ps_output(services_status)
|
2024-10-03 18:21:27 -07:00
|
|
|
return services
|
|
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
2024-10-10 17:44:41 -07:00
|
|
|
log.info(f"Failed to check service status. Error:\n{e.stderr}")
|
2024-10-03 18:21:27 -07:00
|
|
|
return e
|
|
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
|
|
|
|
# Helper method to print service status
|
2024-10-03 18:21:27 -07:00
|
|
|
def print_service_status(services):
|
2024-10-10 17:44:41 -07:00
|
|
|
log.info(f"{'Service Name':<25} {'State':<20} {'Ports'}")
|
|
|
|
|
log.info("=" * 72)
|
2024-10-03 18:21:27 -07:00
|
|
|
for service_name, info in services.items():
|
2024-10-09 11:25:07 -07:00
|
|
|
status = info["STATE"]
|
|
|
|
|
ports = info["PORTS"]
|
2024-10-10 17:44:41 -07:00
|
|
|
log.info(f"{service_name:<25} {status:<20} {ports}")
|
2024-10-03 18:21:27 -07:00
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
|
|
|
|
# check for states based on the states passed in
|
2024-10-03 18:21:27 -07:00
|
|
|
def check_services_state(services, states):
|
|
|
|
|
for service_name, service_info in services.items():
|
2024-10-09 11:25:07 -07:00
|
|
|
status = service_info[
|
|
|
|
|
"STATE"
|
|
|
|
|
].lower() # Convert status to lowercase for easier comparison
|
2024-10-03 18:21:27 -07:00
|
|
|
if any(state in status for state in states):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
return False
|
2024-10-05 10:49:47 -07:00
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
2024-10-05 10:49:47 -07:00
|
|
|
def get_llm_provider_access_keys(arch_config_file):
|
2024-10-09 11:25:07 -07:00
|
|
|
with open(arch_config_file, "r") as file:
|
2024-10-05 10:49:47 -07:00
|
|
|
arch_config = file.read()
|
|
|
|
|
arch_config_yaml = yaml.safe_load(arch_config)
|
|
|
|
|
|
|
|
|
|
access_key_list = []
|
|
|
|
|
for llm_provider in arch_config_yaml.get("llm_providers", []):
|
|
|
|
|
acess_key = llm_provider.get("access_key")
|
|
|
|
|
if acess_key is not None:
|
|
|
|
|
access_key_list.append(acess_key)
|
|
|
|
|
|
|
|
|
|
return access_key_list
|
|
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
2024-10-05 10:49:47 -07:00
|
|
|
def load_env_file_to_dict(file_path):
|
|
|
|
|
env_dict = {}
|
|
|
|
|
|
|
|
|
|
# Open and read the .env file
|
2024-10-09 11:25:07 -07:00
|
|
|
with open(file_path, "r") as file:
|
2024-10-05 10:49:47 -07:00
|
|
|
for line in file:
|
|
|
|
|
# Strip any leading/trailing whitespaces
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
|
|
|
|
# Skip empty lines and comments
|
2024-10-09 11:25:07 -07:00
|
|
|
if not line or line.startswith("#"):
|
2024-10-05 10:49:47 -07:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Split the line into key and value at the first '=' sign
|
2024-10-09 11:25:07 -07:00
|
|
|
if "=" in line:
|
|
|
|
|
key, value = line.split("=", 1)
|
2024-10-05 10:49:47 -07:00
|
|
|
key = key.strip()
|
|
|
|
|
value = value.strip()
|
|
|
|
|
|
|
|
|
|
# Add key-value pair to the dictionary
|
|
|
|
|
env_dict[key] = value
|
|
|
|
|
|
|
|
|
|
return env_dict
|
|
|
|
|
|
2024-10-09 11:25:07 -07:00
|
|
|
|
2024-10-05 10:49:47 -07:00
|
|
|
def parse_docker_compose_ps_output(output):
|
|
|
|
|
# Split the output into lines
|
|
|
|
|
lines = output.strip().splitlines()
|
|
|
|
|
|
|
|
|
|
# Extract the headers (first row) and the rest of the data
|
|
|
|
|
headers = lines[0].split()
|
|
|
|
|
service_data = lines[1:]
|
|
|
|
|
|
|
|
|
|
# Initialize the result dictionary
|
|
|
|
|
services = {}
|
|
|
|
|
|
|
|
|
|
# Iterate over each line of data after the headers
|
|
|
|
|
for line in service_data:
|
|
|
|
|
# Split the line by tabs or multiple spaces
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
|
|
|
|
# Create a dictionary entry using the header names
|
2024-10-09 11:25:07 -07:00
|
|
|
service_info = {headers[1]: parts[1], headers[2]: parts[2]} # State # Ports
|
2024-10-05 10:49:47 -07:00
|
|
|
|
|
|
|
|
# Add to the result dictionary using the service name as the key
|
|
|
|
|
services[parts[0]] = service_info
|
|
|
|
|
|
|
|
|
|
return services
|