Files
10ksim/deployment.py
PearsonWhite 7ce65ab52f Improve experiment config CLI params (#310)
* Automatically generate params for experiment configs

* Easier to extend BaseExperiment subparser with custom args

* Removed the ability to add custom subparser without BaseExperiment flags

* Add check to ensure derived classes do not override add_parser
2026-07-02 08:36:11 -04:00

117 lines
3.5 KiB
Python

# Python Imports
import argparse
import asyncio
import logging
from pathlib import Path
from typing import Optional
from kubernetes import config
from kubernetes.client import ApiClient
from ruamel import yaml
# Project Imports
from src.analysis.utils.log_utils import init_logger
from src.deployments.core.k8s_kubeconfig import set_config_file
from src.deployments.registry import registry as experiment_registry
from src.deployments.utils.parser import ARG_NOT_SET
logger = logging.getLogger(__name__)
async def run_experiment(
name: str,
args: argparse.Namespace,
values_path: Optional[str],
kube_config: Path,
):
logger.debug(f"params: {args}")
config.load_kube_config(config_file=kube_config)
set_config_file(kube_config)
api_client = ApiClient()
try:
with open(values_path, "r") as values:
values_yaml = yaml.safe_load(values.read()) # todo: change to get_YAML().load()...
except TypeError:
# values_path is None.
values_yaml = None
if values_yaml is None:
values_yaml = {}
cli_args = {key: value for key, value in vars(args).items() if value is not ARG_NOT_SET}
info = experiment_registry[name]
experiment = info.cls(
api_client=api_client,
config={**values_yaml, **cli_args},
namespace=args.namespace,
output_folder=args.out_folder,
skip_check=args.skip_check,
dry_run=args.dry_run,
)
logger.info(f"Running experiment. name `{info.name}` file: `{info.metadata['module_path']}`")
await experiment.run()
async def main():
parser = argparse.ArgumentParser(
description="A tool to run experiments. Generates deployment yaml with helm and deploys using the given kubeconfig. Dependencies: helm must be installed and in $PATH."
)
subparsers = parser.add_subparsers(dest="experiment", required=True)
parser.add_argument("--values", default=None, help="Path to values.yaml", dest="values_path")
parser.add_argument(
"--config",
required=False,
help="Config passed to --kubeconfig in kubernetes commands.",
default="~/.kube/config",
dest="kube_config",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
dest="verbosity",
default=0,
help="Set the log level: -v (warnings), -vv (info), -vvv (debug) -vvvv (most verbose)",
)
parser.add_argument(
"-l",
"--out-folder",
type=Path,
dest="out_folder",
default=None,
required=False,
help="Output folder to contain all experiment output. "
"Path is relative to `__file__/out/` unless given as absolute path. "
"Default path uses `__file__/out/{{datetime}}`.",
)
# Scan for experiments.
experiment_registry.scan(Path(__file__) / ".." / "src" / "deployments", mode="skip")
# Add subparsers for all experiments.
for info in experiment_registry.items():
try:
info.cls.add_parser(subparsers)
except AttributeError as e:
raise AttributeError(f"{info}") from e
args = parser.parse_args()
verbosity = args.verbosity or 2
init_logger(logging.getLogger(), verbosity)
try:
await run_experiment(
name=args.experiment,
args=args,
values_path=args.values_path,
kube_config=args.kube_config,
)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
asyncio.run(main())