Skip to content

Commit 71e4925

Browse files
authored
the remote-run missing bits (#4028)
This was meant to be included in the last release but I (luisp) don't know how to read and merged things in the "wrong" order. This correctly merges in the following PRs: * cleanup for remote-run resources (#4024) Clean up logic for the (meant to be) ephemeral resources which are created in remote-run invocations (#4022). * new remote-run cli (#4025) Last portion of #3986, with the updated logic to reflect the updated API conventions introduced in #4022.
1 parent 60cc9aa commit 71e4925

File tree

9 files changed

+571
-20
lines changed

9 files changed

+571
-20
lines changed

debian/paasta-tools.links

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ opt/venvs/paasta-tools/bin/generate_authenticating_services.py usr/bin/generate_
1616
opt/venvs/paasta-tools/bin/kubernetes_remove_evicted_pods.py usr/bin/kubernetes_remove_evicted_pods
1717
opt/venvs/paasta-tools/bin/paasta-api usr/bin/paasta-api
1818
opt/venvs/paasta-tools/bin/paasta-fsm usr/bin/paasta-fsm
19+
opt/venvs/paasta-tools/bin/paasta_cleanup_remote_run_resources.py usr/bin/paasta_cleanup_remote_run_resources
1920
opt/venvs/paasta-tools/bin/paasta_cleanup_stale_nodes.py usr/bin/paasta_cleanup_stale_nodes
2021
opt/venvs/paasta-tools/bin/paasta_prune_completed_pods usr/bin/paasta_prune_completed_pods
2122
opt/venvs/paasta-tools/bin/paasta_cleanup_tron_namespaces usr/bin/paasta_cleanup_tron_namespaces

paasta_tools/cli/cmds/remote_run.py

Lines changed: 182 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,193 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import argparse
16+
import pty
17+
import shlex
18+
import time
1619

17-
from paasta_tools.utils import PaastaColors
20+
from paasta_tools.cli.utils import get_paasta_oapi_api_clustername
21+
from paasta_tools.cli.utils import get_paasta_oapi_client_with_auth
22+
from paasta_tools.cli.utils import lazy_choices_completer
23+
from paasta_tools.paastaapi.model.remote_run_start import RemoteRunStart
24+
from paasta_tools.paastaapi.model.remote_run_stop import RemoteRunStop
25+
from paasta_tools.utils import get_username
26+
from paasta_tools.utils import list_clusters
27+
from paasta_tools.utils import list_services
28+
from paasta_tools.utils import load_system_paasta_config
29+
from paasta_tools.utils import SystemPaastaConfig
1830

1931

20-
def add_subparser(subparsers: argparse._SubParsersAction):
21-
subparsers.add_parser(
32+
KUBECTL_CMD_TEMPLATE = (
33+
"kubectl-eks-{cluster} --token {token} exec -it -n {namespace} {pod} -- /bin/bash"
34+
)
35+
36+
37+
def paasta_remote_run_start(
38+
args: argparse.Namespace,
39+
system_paasta_config: SystemPaastaConfig,
40+
) -> int:
41+
client = get_paasta_oapi_client_with_auth(
42+
cluster=get_paasta_oapi_api_clustername(cluster=args.cluster, is_eks=True),
43+
system_paasta_config=system_paasta_config,
44+
)
45+
if not client:
46+
print("Cannot get a paasta-api client")
47+
return 1
48+
49+
user = get_username()
50+
start_response = client.remote_run.remote_run_start(
51+
args.service,
52+
args.instance,
53+
RemoteRunStart(
54+
user=user,
55+
interactive=args.interactive,
56+
recreate=args.recreate,
57+
max_duration=args.max_duration,
58+
),
59+
)
60+
if start_response.status >= 300:
61+
print(f"Error from PaaSTA APIs while starting job: {start_response.message}")
62+
return 1
63+
64+
start_time = time.time()
65+
while time.time() - start_time < args.timeout:
66+
poll_response = client.remote_run.remote_run_poll(
67+
args.service,
68+
args.instance,
69+
start_response.job_name,
70+
)
71+
if poll_response.status == 200:
72+
break
73+
time.sleep(10)
74+
else:
75+
print("Timed out while waiting for job to start")
76+
return 1
77+
78+
if not args.interactive:
79+
print("Successfully started remote-run job")
80+
return 0
81+
82+
token_response = client.remote_run.remote_run_token(
83+
args.service, args.instance, user
84+
)
85+
86+
exec_command = KUBECTL_CMD_TEMPLATE.format(
87+
cluster=args.cluster,
88+
namespace=poll_response.namespace,
89+
pod=poll_response.pod_name,
90+
token=token_response.token,
91+
)
92+
pty.spawn(shlex.split(exec_command))
93+
return 0
94+
95+
96+
def paasta_remote_run_stop(
97+
args: argparse.Namespace,
98+
system_paasta_config: SystemPaastaConfig,
99+
) -> int:
100+
client = get_paasta_oapi_client_with_auth(
101+
cluster=get_paasta_oapi_api_clustername(cluster=args.cluster, is_eks=True),
102+
system_paasta_config=system_paasta_config,
103+
)
104+
if not client:
105+
print("Cannot get a paasta-api client")
106+
return 1
107+
response = client.remote_run.remote_run_stop(
108+
args.service, args.instance, RemoteRunStop(user=get_username())
109+
)
110+
print(response.message)
111+
return 0 if response.status < 300 else 1
112+
113+
114+
def add_common_args_to_parser(parser: argparse.ArgumentParser):
115+
service_arg = parser.add_argument(
116+
"-s",
117+
"--service",
118+
help="The name of the service you wish to inspect. Required.",
119+
required=True,
120+
)
121+
service_arg.completer = lazy_choices_completer(list_services) # type: ignore
122+
parser.add_argument(
123+
"-i",
124+
"--instance",
125+
help=(
126+
"Simulate a docker run for a particular instance of the "
127+
"service, like 'main' or 'canary'. Required."
128+
),
129+
required=True,
130+
)
131+
cluster_arg = parser.add_argument(
132+
"-c",
133+
"--cluster",
134+
help="The name of the cluster you wish to run your task on. Required.",
135+
required=True,
136+
)
137+
cluster_arg.completer = lazy_choices_completer(list_clusters) # type: ignore
138+
139+
140+
def add_subparser(subparsers: argparse._SubParsersAction) -> None:
141+
remote_run_parser = subparsers.add_parser(
22142
"remote-run",
23-
help="Schedule adhoc service sandbox on PaaSTA cluster",
24-
description=(
25-
"`paasta remote-run` is useful for running adhoc commands in "
26-
"context of a service's Docker image."
143+
help="Run services / jobs remotely",
144+
description="'paasta remote-run' runs services / jobs remotely",
145+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
146+
)
147+
subparsers = remote_run_parser.add_subparsers(dest="remote_run_command")
148+
start_parser = subparsers.add_parser(
149+
"start",
150+
help="Start or connect to a remote-run job",
151+
description="Starts or connects to a remote-run-job",
152+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
153+
)
154+
start_parser.add_argument(
155+
"-I",
156+
"--interactive",
157+
help=(
158+
"Run container in interactive mode. If interactive is set the "
159+
'default command will be "bash" unless otherwise set by the "--cmd" flag'
160+
),
161+
action="store_true",
162+
default=False,
163+
)
164+
start_parser.add_argument(
165+
"-m",
166+
"--max-duration",
167+
help=(
168+
"Amount of time in seconds after which the job is "
169+
"automatically stopped (capped by the API backend)"
27170
),
171+
type=int,
172+
default=1800,
173+
)
174+
start_parser.add_argument(
175+
"-r",
176+
"--recreate",
177+
help="Recreate remote-run job if already existing",
178+
action="store_true",
179+
default=False,
180+
)
181+
start_parser.add_argument(
182+
"-t",
183+
"--timeout",
184+
help="Maximum time to wait for a job to start, in seconds",
185+
type=int,
186+
default=600,
187+
)
188+
stop_parser = subparsers.add_parser(
189+
"stop",
190+
help="Stop your remote-run job if it exists",
191+
description="Stop your remote-run job if it exists",
192+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
28193
)
194+
add_common_args_to_parser(start_parser)
195+
add_common_args_to_parser(stop_parser)
196+
remote_run_parser.set_defaults(command=paasta_remote_run)
29197

30198

31-
def paasta_remote_run(args: argparse.Namespace):
32-
print(PaastaColors.red("Error: functionality under construction"))
33-
return 1
199+
def paasta_remote_run(args: argparse.Namespace) -> int:
200+
system_paasta_config = load_system_paasta_config()
201+
if args.remote_run_command == "start":
202+
return paasta_remote_run_start(args, system_paasta_config)
203+
elif args.remote_run_command == "stop":
204+
return paasta_remote_run_stop(args, system_paasta_config)
205+
raise ValueError(f"Unsupported subcommand: {args.remote_run_command}")

paasta_tools/cli/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343

4444
from paasta_tools import remote_git
4545
from paasta_tools.adhoc_tools import load_adhoc_job_config
46+
from paasta_tools.api.client import get_paasta_oapi_client
47+
from paasta_tools.api.client import PaastaOApiClient
4648
from paasta_tools.cassandracluster_tools import load_cassandracluster_instance_config
4749
from paasta_tools.eks_tools import EksDeploymentConfig
4850
from paasta_tools.eks_tools import load_eks_service_config
@@ -1135,3 +1137,16 @@ def get_sso_service_auth_token() -> str:
11351137
"""Generate an authentication token for the calling user from the Single Sign On provider"""
11361138
client_id = load_system_paasta_config().get_service_auth_sso_oidc_client_id()
11371139
return get_and_cache_jwt_default(client_id)
1140+
1141+
1142+
def get_paasta_oapi_client_with_auth(
1143+
cluster: str = None,
1144+
system_paasta_config: SystemPaastaConfig = None,
1145+
http_res: bool = False,
1146+
) -> Optional[PaastaOApiClient]:
1147+
return get_paasta_oapi_client(
1148+
cluster=cluster,
1149+
system_paasta_config=system_paasta_config,
1150+
http_res=http_res,
1151+
auth_token=get_sso_service_auth_token(),
1152+
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
# Copyright 2015-2019 Yelp Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
import argparse
16+
from datetime import datetime
17+
from datetime import timedelta
18+
from datetime import timezone
19+
from typing import Any
20+
from typing import Callable
21+
from typing import Sequence
22+
from typing import Tuple
23+
24+
from paasta_tools.kubernetes.remote_run import get_remote_run_role_bindings
25+
from paasta_tools.kubernetes.remote_run import get_remote_run_roles
26+
from paasta_tools.kubernetes.remote_run import get_remote_run_service_accounts
27+
from paasta_tools.kubernetes_tools import get_all_managed_namespaces
28+
from paasta_tools.kubernetes_tools import KubeClient
29+
30+
31+
ListingFuncType = Callable[[KubeClient, str], Sequence[Any]]
32+
DeletionFuncType = Callable[[str, str], Any]
33+
34+
35+
def clean_namespace(kube_client: KubeClient, namespace: str, age_limit: datetime):
36+
"""Clean ephemeral remote-run resource in a namespace
37+
38+
:param KubeClient kube_client: kubernetes client
39+
:param str namepsace: kubernetes namespace
40+
:param datetime age_limit: expiration time for resources
41+
"""
42+
cleanup_actions: Sequence[Tuple[DeletionFuncType, ListingFuncType]] = (
43+
(
44+
kube_client.core.delete_namespaced_service_account,
45+
get_remote_run_service_accounts,
46+
),
47+
(kube_client.rbac.delete_namespaced_role, get_remote_run_roles),
48+
(kube_client.rbac.delete_namespaced_role_binding, get_remote_run_role_bindings),
49+
)
50+
for delete_func, list_func in cleanup_actions:
51+
for entity in list_func(kube_client, namespace):
52+
if (
53+
not entity.metadata.name.startswith("remote-run-")
54+
or entity.metadata.creation_timestamp > age_limit
55+
):
56+
continue
57+
delete_func(entity.metadata.name, namespace)
58+
59+
60+
def parse_args() -> argparse.Namespace:
61+
parser = argparse.ArgumentParser(
62+
description="Clean ephemeral Kubernetes resources created by remote-run invocations",
63+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
64+
)
65+
parser.add_argument(
66+
"--max-age",
67+
type=int,
68+
default=600,
69+
help="Maximum age, in seconds, resources are allowed to have",
70+
)
71+
return parser.parse_args()
72+
73+
74+
def main():
75+
args = parse_args()
76+
kube_client = KubeClient()
77+
age_limit = datetime.now(tzinfo=timezone.utc) - timedelta(seconds=args.max_age)
78+
for namespace in get_all_managed_namespaces(kube_client):
79+
clean_namespace(kube_client, namespace, age_limit)
80+
81+
82+
if __name__ == "__main__":
83+
main()

0 commit comments

Comments
 (0)