|
| 1 | +import json |
| 2 | +import subprocess |
| 3 | + |
| 4 | +from .helpers import run_command |
| 5 | + |
| 6 | +REGION = "eu-west-2" |
| 7 | +PRODUCTION_ENVS = {"production", "production-data-replication"} |
| 8 | + |
| 9 | + |
| 10 | +def cluster(env): |
| 11 | + return f"mavis-{env}" |
| 12 | + |
| 13 | + |
| 14 | +def s3_bucket(env): |
| 15 | + if env in PRODUCTION_ENVS: |
| 16 | + return "mavis-filetransfer-production" |
| 17 | + return "mavis-filetransfer-development" |
| 18 | + |
| 19 | + |
| 20 | +def ensure_authenticated(exit_without_login=False): |
| 21 | + """Check AWS auth; attempt SSO login if needed.""" |
| 22 | + result = subprocess.run( |
| 23 | + ["aws", "sts", "get-caller-identity"], |
| 24 | + capture_output=True, |
| 25 | + ) |
| 26 | + if result.returncode == 0: |
| 27 | + return |
| 28 | + if exit_without_login: |
| 29 | + raise RuntimeError( |
| 30 | + "Not authenticated with AWS. Run 'aws sso login' and try again." |
| 31 | + ) |
| 32 | + print("Not authenticated with AWS. Attempting SSO login...") |
| 33 | + login = subprocess.run(["aws", "sso", "login"]) |
| 34 | + if login.returncode != 0: |
| 35 | + raise RuntimeError("AWS SSO login failed.") |
| 36 | + recheck = subprocess.run( |
| 37 | + ["aws", "sts", "get-caller-identity"], |
| 38 | + capture_output=True, |
| 39 | + ) |
| 40 | + if recheck.returncode != 0: |
| 41 | + raise RuntimeError("Still not authenticated after SSO login.") |
| 42 | + |
| 43 | + |
| 44 | +def aws_json(*cmd): |
| 45 | + """Run an AWS CLI command and return parsed JSON output.""" |
| 46 | + result = subprocess.run(["aws", *cmd], capture_output=True, text=True) |
| 47 | + if result.returncode != 0: |
| 48 | + raise RuntimeError(f"aws {' '.join(cmd)}:\n{result.stderr.strip()}") |
| 49 | + return json.loads(result.stdout) |
| 50 | + |
| 51 | + |
| 52 | +def resolve_task(env, task_id=None, task_ip=None, service=None): |
| 53 | + """ |
| 54 | + Resolve to (short_task_id, container_name). Three mutually exclusive modes: |
| 55 | +
|
| 56 | + - task_id — validate the specific task is running |
| 57 | + - task_ip — search all running tasks in the cluster for a matching IP |
| 58 | + - service — return the first running task in the service; defaults to |
| 59 | + mavis-{env}-ops, or mavis-{env}-web for data-replication envs |
| 60 | + """ |
| 61 | + cl = cluster(env) |
| 62 | + |
| 63 | + if task_id: |
| 64 | + tasks = aws_json( |
| 65 | + "ecs", |
| 66 | + "describe-tasks", |
| 67 | + "--region", |
| 68 | + REGION, |
| 69 | + "--cluster", |
| 70 | + cl, |
| 71 | + "--tasks", |
| 72 | + task_id, |
| 73 | + ).get("tasks", []) |
| 74 | + if not tasks or tasks[0]["lastStatus"] != "RUNNING": |
| 75 | + raise RuntimeError(f"Task {task_id} is not running in cluster {cl}") |
| 76 | + return task_id, _application_container(tasks[0]) |
| 77 | + |
| 78 | + if task_ip: |
| 79 | + task_arns = aws_json( |
| 80 | + "ecs", |
| 81 | + "list-tasks", |
| 82 | + "--region", |
| 83 | + REGION, |
| 84 | + "--cluster", |
| 85 | + cl, |
| 86 | + "--desired-status", |
| 87 | + "RUNNING", |
| 88 | + ).get("taskArns", []) |
| 89 | + if not task_arns: |
| 90 | + raise RuntimeError(f"No running tasks found in cluster {cl}") |
| 91 | + tasks = aws_json( |
| 92 | + "ecs", |
| 93 | + "describe-tasks", |
| 94 | + "--region", |
| 95 | + REGION, |
| 96 | + "--cluster", |
| 97 | + cl, |
| 98 | + "--tasks", |
| 99 | + *task_arns, |
| 100 | + ).get("tasks", []) |
| 101 | + for task in tasks: |
| 102 | + if _task_private_ip(task) == task_ip: |
| 103 | + return _short_id(task), _application_container(task) |
| 104 | + raise RuntimeError(f"No running task with IP {task_ip} found in cluster {cl}") |
| 105 | + |
| 106 | + if not service: |
| 107 | + service = _default_service(env) |
| 108 | + |
| 109 | + task_arns = aws_json( |
| 110 | + "ecs", |
| 111 | + "list-tasks", |
| 112 | + "--region", |
| 113 | + REGION, |
| 114 | + "--cluster", |
| 115 | + cl, |
| 116 | + "--service-name", |
| 117 | + service, |
| 118 | + "--desired-status", |
| 119 | + "RUNNING", |
| 120 | + ).get("taskArns", []) |
| 121 | + if not task_arns: |
| 122 | + raise RuntimeError(f"No running tasks found in service {service}") |
| 123 | + tasks = aws_json( |
| 124 | + "ecs", |
| 125 | + "describe-tasks", |
| 126 | + "--region", |
| 127 | + REGION, |
| 128 | + "--cluster", |
| 129 | + cl, |
| 130 | + "--tasks", |
| 131 | + *task_arns, |
| 132 | + ).get("tasks", []) |
| 133 | + for task in tasks: |
| 134 | + container = _application_container(task) |
| 135 | + if container: |
| 136 | + return _short_id(task), container |
| 137 | + raise RuntimeError( |
| 138 | + f"No running tasks with an application container found in service {service}" |
| 139 | + ) |
| 140 | + |
| 141 | + |
| 142 | +def run_remote_command( |
| 143 | + env, task_id, remote_command, container=None, replace_process=False |
| 144 | +): |
| 145 | + """Execute a command in an ECS task, returning the exit code.""" |
| 146 | + command = [ |
| 147 | + "aws", |
| 148 | + "ecs", |
| 149 | + "execute-command", |
| 150 | + "--region", |
| 151 | + REGION, |
| 152 | + "--cluster", |
| 153 | + cluster(env), |
| 154 | + "--task", |
| 155 | + task_id, |
| 156 | + "--command", |
| 157 | + remote_command, |
| 158 | + "--interactive", |
| 159 | + ] |
| 160 | + if container: |
| 161 | + command += ["--container", container] |
| 162 | + return run_command(command, replace_process=replace_process) |
| 163 | + |
| 164 | + |
| 165 | +# --- private helpers --- |
| 166 | + |
| 167 | + |
| 168 | +def _default_service(env): |
| 169 | + if env.endswith("data-replication"): |
| 170 | + return f"mavis-{env}" |
| 171 | + return f"mavis-{env}-ops" |
| 172 | + |
| 173 | + |
| 174 | +def _short_id(task): |
| 175 | + return task["taskArn"].split("/")[-1] |
| 176 | + |
| 177 | + |
| 178 | +def _application_container(task): |
| 179 | + for c in task.get("containers", []): |
| 180 | + if ( |
| 181 | + c.get("name") == "application" |
| 182 | + and c.get("lastStatus") == "RUNNING" |
| 183 | + and c.get("runtimeId") |
| 184 | + ): |
| 185 | + return c["name"] |
| 186 | + return None |
| 187 | + |
| 188 | + |
| 189 | +def _task_private_ip(task): |
| 190 | + for attachment in task.get("attachments", []): |
| 191 | + for detail in attachment.get("details", []): |
| 192 | + if detail.get("name") == "privateIPv4Address": |
| 193 | + return detail.get("value") |
| 194 | + return None |
0 commit comments