paasta-tools 1.30.9__py3-none-any.whl → 1.35.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of paasta-tools might be problematic. Click here for more details.

Files changed (98) hide show
  1. paasta_tools/__init__.py +1 -1
  2. paasta_tools/api/api_docs/swagger.json +5 -0
  3. paasta_tools/cli/cmds/autoscale.py +2 -0
  4. paasta_tools/cli/cmds/check.py +2 -0
  5. paasta_tools/cli/cmds/cook_image.py +2 -0
  6. paasta_tools/cli/cmds/get_docker_image.py +2 -0
  7. paasta_tools/cli/cmds/get_image_version.py +2 -0
  8. paasta_tools/cli/cmds/get_latest_deployment.py +2 -0
  9. paasta_tools/cli/cmds/info.py +5 -1
  10. paasta_tools/cli/cmds/itest.py +2 -0
  11. paasta_tools/cli/cmds/list_namespaces.py +2 -0
  12. paasta_tools/cli/cmds/local_run.py +116 -24
  13. paasta_tools/cli/cmds/logs.py +2 -0
  14. paasta_tools/cli/cmds/mark_for_deployment.py +12 -2
  15. paasta_tools/cli/cmds/mesh_status.py +2 -1
  16. paasta_tools/cli/cmds/push_to_registry.py +2 -0
  17. paasta_tools/cli/cmds/remote_run.py +10 -0
  18. paasta_tools/cli/cmds/rollback.py +5 -1
  19. paasta_tools/cli/cmds/secret.py +4 -2
  20. paasta_tools/cli/cmds/security_check.py +2 -0
  21. paasta_tools/cli/cmds/spark_run.py +4 -0
  22. paasta_tools/cli/cmds/status.py +35 -8
  23. paasta_tools/cli/cmds/validate.py +296 -19
  24. paasta_tools/cli/cmds/wait_for_deployment.py +2 -0
  25. paasta_tools/cli/schemas/autoscaling_schema.json +3 -2
  26. paasta_tools/cli/schemas/eks_schema.json +23 -1
  27. paasta_tools/cli/schemas/smartstack_schema.json +12 -0
  28. paasta_tools/cli/utils.py +2 -1
  29. paasta_tools/contrib/paasta_update_soa_memcpu.py +10 -14
  30. paasta_tools/generate_deployments_for_service.py +2 -0
  31. paasta_tools/instance/hpa_metrics_parser.py +3 -5
  32. paasta_tools/instance/kubernetes.py +58 -25
  33. paasta_tools/kubernetes/application/controller_wrappers.py +23 -2
  34. paasta_tools/kubernetes/remote_run.py +2 -2
  35. paasta_tools/kubernetes_tools.py +37 -66
  36. paasta_tools/long_running_service_tools.py +8 -1
  37. paasta_tools/paastaapi/model/kubernetes_version.py +3 -0
  38. paasta_tools/setup_prometheus_adapter_config.py +82 -0
  39. paasta_tools/tron_tools.py +3 -0
  40. paasta_tools/utils.py +26 -9
  41. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/generate_deployments_for_service.py +2 -0
  42. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_update_soa_memcpu.py +10 -14
  43. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_prometheus_adapter_config.py +82 -0
  44. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/METADATA +4 -4
  45. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/RECORD +98 -98
  46. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/apply_external_resources.py +0 -0
  47. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/bounce_log_latency_parser.py +0 -0
  48. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_autoscaler_max_instances.py +0 -0
  49. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_cassandracluster_services_replication.py +0 -0
  50. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_flink_services_health.py +0 -0
  51. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_kubernetes_api.py +0 -0
  52. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_kubernetes_services_replication.py +0 -0
  53. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_manual_oapi_changes.sh +0 -0
  54. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_oom_events.py +0 -0
  55. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_orphans.py +0 -0
  56. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/check_spark_jobs.py +0 -0
  57. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/cleanup_kubernetes_cr.py +0 -0
  58. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/cleanup_kubernetes_crd.py +0 -0
  59. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/cleanup_kubernetes_jobs.py +0 -0
  60. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/create_dynamodb_table.py +0 -0
  61. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/create_paasta_playground.py +0 -0
  62. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/delete_kubernetes_deployments.py +0 -0
  63. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/emit_allocated_cpu_metrics.py +0 -0
  64. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/generate_all_deployments +0 -0
  65. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/generate_authenticating_services.py +0 -0
  66. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/generate_services_file.py +0 -0
  67. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/generate_services_yaml.py +0 -0
  68. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/get_running_task_allocation.py +0 -0
  69. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/habitat_fixer.py +0 -0
  70. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/ide_helper.py +0 -0
  71. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/is_pod_healthy_in_proxy.py +0 -0
  72. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/is_pod_healthy_in_smartstack.py +0 -0
  73. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/kill_bad_containers.py +0 -0
  74. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/kubernetes_remove_evicted_pods.py +0 -0
  75. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/mass-deploy-tag.sh +0 -0
  76. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/mock_patch_checker.py +0 -0
  77. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_cleanup_remote_run_resources.py +0 -0
  78. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_cleanup_stale_nodes.py +0 -0
  79. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_deploy_tron_jobs +0 -0
  80. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_execute_docker_command.py +0 -0
  81. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_secrets_sync.py +0 -0
  82. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/paasta_tabcomplete.sh +0 -0
  83. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/render_template.py +0 -0
  84. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/rightsizer_soaconfigs_update.py +0 -0
  85. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/service_shard_remove.py +0 -0
  86. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/service_shard_update.py +0 -0
  87. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_istio_mesh.py +0 -0
  88. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_kubernetes_cr.py +0 -0
  89. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_kubernetes_crd.py +0 -0
  90. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_kubernetes_internal_crd.py +0 -0
  91. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/setup_kubernetes_job.py +0 -0
  92. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/shared_ip_check.py +0 -0
  93. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/synapse_srv_namespaces_fact.py +0 -0
  94. {paasta_tools-1.30.9.data → paasta_tools-1.35.8.data}/scripts/timeouts_metrics_prom.py +0 -0
  95. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/WHEEL +0 -0
  96. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/entry_points.txt +0 -0
  97. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/licenses/LICENSE +0 -0
  98. {paasta_tools-1.30.9.dist-info → paasta_tools-1.35.8.dist-info}/top_level.txt +0 -0
paasta_tools/__init__.py CHANGED
@@ -17,4 +17,4 @@
17
17
  # setup phase, the dependencies may not exist on disk yet.
18
18
  #
19
19
  # Don't bump version manually. See `make release` docs in ./Makefile
20
- __version__ = "1.30.9"
20
+ __version__ = "1.35.8"
@@ -1788,6 +1788,11 @@
1788
1788
  "KubernetesVersion": {
1789
1789
  "type": "object",
1790
1790
  "properties": {
1791
+ "container_port": {
1792
+ "description": "Port the container is expecting to receive traffic on",
1793
+ "type": "integer",
1794
+ "format": "int32"
1795
+ },
1791
1796
  "type": {
1792
1797
  "description": "Type of version (ReplicaSet or ControllerRevision)",
1793
1798
  "type": "string"
@@ -48,6 +48,8 @@ def add_subparser(subparsers):
48
48
  "-s",
49
49
  "--service",
50
50
  help="Service that you want to autoscale. Like 'example_service'.",
51
+ # strip any potential trailing / for folks tab-completing directories
52
+ type=lambda x: x.rstrip("/"),
51
53
  ).completer = lazy_choices_completer(list_services)
52
54
  autoscale_parser.add_argument(
53
55
  "-i",
@@ -53,6 +53,8 @@ def add_subparser(subparsers):
53
53
  "-s",
54
54
  "--service",
55
55
  help="The name of the service you wish to inspect. Defaults to autodetect.",
56
+ # strip any potential trailing / for folks tab-completing directories
57
+ type=lambda x: x.rstrip("/"),
56
58
  ).completer = lazy_choices_completer(list_services)
57
59
  check_parser.add_argument(
58
60
  "-y",
@@ -48,6 +48,8 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
48
48
  '"services-", as included in a Jenkins job name, '
49
49
  "will be stripped."
50
50
  ),
51
+ # strip any potential trailing / for folks tab-completing directories
52
+ type=lambda x: x.rstrip("/"),
51
53
  required=True,
52
54
  )
53
55
  list_parser.add_argument(
@@ -34,6 +34,8 @@ def add_subparser(subparsers):
34
34
  "--service",
35
35
  help="Name of the service which you want to get the docker image for.",
36
36
  required=True,
37
+ # strip any potential trailing / for folks tab-completing directories
38
+ type=lambda x: x.rstrip("/"),
37
39
  ).completer = lazy_choices_completer(list_services)
38
40
  list_parser.add_argument(
39
41
  "-i",
@@ -54,6 +54,8 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
54
54
  "--service",
55
55
  help="Name of the service which you want to get the image version for.",
56
56
  required=True,
57
+ # strip any potential trailing / for folks tab-completing directories
58
+ type=lambda x: x.rstrip("/"),
57
59
  )
58
60
  arg_service.completer = lazy_choices_completer(list_services) # type: ignore
59
61
  parser.add_argument(
@@ -33,6 +33,8 @@ def add_subparser(subparsers):
33
33
  "--service",
34
34
  help="Name of the service which you want to get the latest deployment for.",
35
35
  required=True,
36
+ # strip any potential trailing / for folks tab-completing directories
37
+ type=lambda x: x.rstrip("/"),
36
38
  ).completer = lazy_choices_completer(list_services)
37
39
  list_parser.add_argument(
38
40
  "-i",
@@ -51,7 +51,11 @@ def add_subparser(subparsers):
51
51
  ),
52
52
  )
53
53
  list_parser.add_argument(
54
- "-s", "--service", help="The name of the service you wish to inspect"
54
+ "-s",
55
+ "--service",
56
+ help="The name of the service you wish to inspect",
57
+ # strip any potential trailing / for folks tab-completing directories
58
+ type=lambda x: x.rstrip("/"),
55
59
  ).completer = lazy_choices_completer(list_services)
56
60
  list_parser.add_argument(
57
61
  "-d",
@@ -41,6 +41,8 @@ def add_subparser(subparsers):
41
41
  help="Test and build docker image for this service. Leading "
42
42
  '"services-", as included in a Jenkins job name, '
43
43
  "will be stripped.",
44
+ # strip any potential trailing / for folks tab-completing directories
45
+ type=lambda x: x.rstrip("/"),
44
46
  required=True,
45
47
  )
46
48
  list_parser.add_argument(
@@ -31,6 +31,8 @@ def add_subparser(subparsers) -> None:
31
31
  "--service",
32
32
  help="Name of the service which you want to list the namespaces for.",
33
33
  required=True,
34
+ # strip any potential trailing / for folks tab-completing directories
35
+ type=lambda x: x.rstrip("/"),
34
36
  ).completer = lazy_choices_completer(list_services)
35
37
  # Most services likely don't need to filter by cluster/instance, and can add namespaces from all instances
36
38
  list_parser.add_argument(
@@ -12,6 +12,7 @@
12
12
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
+ import base64
15
16
  import datetime
16
17
  import json
17
18
  import os
@@ -31,7 +32,9 @@ from urllib.parse import urlparse
31
32
  import boto3
32
33
  import requests
33
34
  from docker import errors
35
+ from docker.api.client import APIClient
34
36
  from mypy_extensions import TypedDict
37
+ from service_configuration_lib import read_service_configuration
35
38
 
36
39
  from paasta_tools.adhoc_tools import get_default_interactive_config
37
40
  from paasta_tools.cli.authentication import get_service_auth_token
@@ -325,7 +328,11 @@ def add_subparser(subparsers):
325
328
  ),
326
329
  )
327
330
  list_parser.add_argument(
328
- "-s", "--service", help="The name of the service you wish to inspect"
331
+ "-s",
332
+ "--service",
333
+ help="The name of the service you wish to inspect",
334
+ # strip any potential trailing / for folks tab-completing directories
335
+ type=lambda x: x.rstrip("/"),
329
336
  ).completer = lazy_choices_completer(list_services)
330
337
  list_parser.add_argument(
331
338
  "-c",
@@ -612,26 +619,83 @@ class LostContainerException(Exception):
612
619
  pass
613
620
 
614
621
 
615
- def docker_pull_image(docker_url):
616
- """Pull an image via ``docker pull``. Uses the actual pull command instead of the python
617
- bindings due to the docker auth/registry transition. Once we are past Docker 1.6
618
- we can use better credential management, but for now this function assumes the
619
- user running the command has already been authorized for the registry"""
622
+ class DockerAuthConfig(TypedDict):
623
+ username: str
624
+ password: str
625
+
626
+
627
+ def get_readonly_docker_registry_auth_config(
628
+ docker_url: str,
629
+ ) -> DockerAuthConfig | None:
630
+ system_paasta_config = load_system_paasta_config()
631
+ config_path = system_paasta_config.get_readonly_docker_registry_auth_file()
632
+
633
+ try:
634
+ with open(config_path) as f:
635
+ docker_config = json.load(f)
636
+ except Exception:
637
+ print(
638
+ PaastaColors.yellow(
639
+ "Warning: unable to load read-only docker registry credentials."
640
+ ),
641
+ file=sys.stderr,
642
+ )
643
+ # the best we can do is try to pull with whatever auth the user has configured locally
644
+ # i.e., root-owned docker config in /root/.docker/config.json
645
+ return None
646
+ registry = docker_url.split("/")[0]
647
+
648
+ # find matching auth config - our usual ro config will have at least two entries
649
+ # at the time this comment was written
650
+ auths = docker_config
651
+ for auth_url, auth_data in auths.items():
652
+ if registry in auth_url:
653
+ # Decode the base64 auth string if present
654
+ if "auth" in auth_data:
655
+ auth_string = base64.b64decode(auth_data["auth"]).decode("utf-8")
656
+ username, password = auth_string.split(":", 1)
657
+ return {"username": username, "password": password}
658
+
659
+ # we'll hit this for registries like docker-dev or extra-private internal registries
660
+ return None
661
+
662
+
663
+ def docker_pull_image(docker_client: APIClient, docker_url: str) -> None:
664
+ """Pull an image using the docker-py library with read-only registry credentials"""
620
665
  print(
621
- "Please wait while the image (%s) is pulled (times out after 30m)..."
622
- % docker_url,
666
+ f"Please wait while the image ({docker_url}) is pulled (times out after 30m)...",
623
667
  file=sys.stderr,
624
668
  )
625
- with Timeout(
626
- seconds=1800, error_message=f"Timed out pulling docker image from {docker_url}"
627
- ), open(os.devnull, mode="wb") as DEVNULL:
628
- ret, _ = _run("docker pull %s" % docker_url, stream=True, stdin=DEVNULL)
629
- if ret != 0:
630
- print(
631
- "\nPull failed. Are you authorized to run docker commands?",
632
- file=sys.stderr,
633
- )
634
- sys.exit(ret)
669
+
670
+ auth_config = get_readonly_docker_registry_auth_config(docker_url)
671
+ if not auth_config:
672
+ print(
673
+ PaastaColors.yellow(
674
+ "Warning: No read-only docker registry credentials found, attempting to pull without them."
675
+ ),
676
+ file=sys.stderr,
677
+ )
678
+
679
+ try:
680
+ with Timeout(
681
+ seconds=1800,
682
+ error_message=f"Timed out pulling docker image from {docker_url}",
683
+ ):
684
+ # this is slightly funky since pull() returns the output line-by-line, but as a dict
685
+ # ...that we then need to format back to the usual `docker pull` output
686
+ # :p
687
+ for line in docker_client.pull(
688
+ docker_url, auth_config=auth_config, stream=True, decode=True
689
+ ):
690
+ # not all lines have an 'id' key :(
691
+ id_prefix = f"{line['id']}: " if "id" in line else ""
692
+ print(f"{id_prefix}{line['status']}", file=sys.stderr)
693
+ except Exception as e:
694
+ print(
695
+ f"\nPull failed. Error: {e}",
696
+ file=sys.stderr,
697
+ )
698
+ sys.exit(1)
635
699
 
636
700
 
637
701
  def get_container_id(docker_client, container_name):
@@ -1226,7 +1290,7 @@ def configure_and_run_docker_container(
1226
1290
  return 1
1227
1291
 
1228
1292
  if pull_image:
1229
- docker_pull_image(docker_url)
1293
+ docker_pull_image(docker_client, docker_url)
1230
1294
 
1231
1295
  for volume in instance_config.get_volumes(
1232
1296
  system_paasta_config.get_volumes(),
@@ -1302,10 +1366,40 @@ def docker_config_available():
1302
1366
  )
1303
1367
 
1304
1368
 
1369
+ def should_reexec_as_root(
1370
+ service: str, skip_secrets: bool, action: str, soa_dir: str = DEFAULT_SOA_DIR
1371
+ ) -> bool:
1372
+ # local-run can't pull secrets from Vault in prod without a root-owned token
1373
+ need_vault_token = not skip_secrets and action == "pull"
1374
+
1375
+ # there are some special teams with their own private docker registries and no ro creds
1376
+ # however, we don't know what registry is to be used without loading the service config
1377
+ service_info = read_service_configuration(service, soa_dir)
1378
+ # technically folks can set the standard registry as a value here, but atm no one is doing that :p
1379
+ registry_override = service_info.get("docker_registry")
1380
+ # note: we could also have a list of registries that have ro creds, but this seems fine for now
1381
+ uses_private_registry = (
1382
+ registry_override
1383
+ and registry_override
1384
+ in load_system_paasta_config().get_private_docker_registries()
1385
+ )
1386
+ need_docker_config = uses_private_registry and action == "pull"
1387
+
1388
+ return (need_vault_token or need_docker_config) and os.geteuid() != 0
1389
+
1390
+
1305
1391
  def paasta_local_run(args):
1306
- if args.action == "pull" and os.geteuid() != 0 and not docker_config_available():
1307
- print("Re-executing paasta local-run --pull with sudo..")
1308
- os.execvp("sudo", ["sudo", "-H"] + sys.argv)
1392
+ service = figure_out_service_name(args, soa_dir=args.yelpsoa_config_root)
1393
+ if should_reexec_as_root(
1394
+ service, args.skip_secrets, args.action, args.yelpsoa_config_root
1395
+ ):
1396
+ # XXX: we should re-architect this to not need sudo, but for now,
1397
+ # re-exec ourselves with sudo to get access to the paasta vault token
1398
+ # NOTE: once we do that, we can also remove the venv check above :)
1399
+ print(
1400
+ "Re-executing paasta local-run --pull with sudo for Vault/Docker registry access..."
1401
+ )
1402
+ os.execvp("sudo", ["sudo", "-H", "/usr/bin/paasta"] + sys.argv[1:])
1309
1403
  if args.action == "build" and not makefile_responds_to("cook-image"):
1310
1404
  print(
1311
1405
  "A local Makefile with a 'cook-image' target is required for --build",
@@ -1332,8 +1426,6 @@ def paasta_local_run(args):
1332
1426
 
1333
1427
  local_run_config = system_paasta_config.get_local_run_config()
1334
1428
 
1335
- service = figure_out_service_name(args, soa_dir=args.yelpsoa_config_root)
1336
-
1337
1429
  if args.cluster:
1338
1430
  cluster = args.cluster
1339
1431
  else:
@@ -117,6 +117,8 @@ def add_subparser(subparsers) -> None:
117
117
  "-s",
118
118
  "--service",
119
119
  help="The name of the service you wish to inspect. Defaults to autodetect.",
120
+ # strip any potential trailing / for folks tab-completing directories
121
+ type=lambda x: x.rstrip("/"),
120
122
  ).completer = lazy_choices_completer(list_services)
121
123
  status_parser.add_argument(
122
124
  "-c",
@@ -157,6 +157,8 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
157
157
  help="Name of the service which you wish to mark for deployment. Leading "
158
158
  '"services-" will be stripped.',
159
159
  required=True,
160
+ # strip any potential trailing / for folks tab-completing directories
161
+ type=lambda x: x.rstrip("/"),
160
162
  )
161
163
  arg_service.completer = lazy_choices_completer(list_services) # type: ignore
162
164
  list_parser.add_argument(
@@ -1264,7 +1266,15 @@ class MarkForDeploymentProcess(RollbackSlackDeploymentProcess):
1264
1266
  self.ping_authors()
1265
1267
 
1266
1268
  def send_manual_rollback_instructions(self) -> None:
1267
- if self.deployment_version != self.old_deployment_version:
1269
+ # NOTE: new deploy groups are not exactly a particularly frequent occurrence, but
1270
+ # we want to prevent sending messages that look like
1271
+ # `If you need to roll back manually, run: paasta rollback --service $S --deploy-group $G --commit None`
1272
+ # since that's not actually valid/actionable.
1273
+ # thus the seemingly out-of-nowhere old_git_sha check: new deploy groups won't have value set there.
1274
+ if (
1275
+ self.deployment_version != self.old_deployment_version
1276
+ and self.old_git_sha is not None
1277
+ ):
1268
1278
  extra_rollback_args = ""
1269
1279
  if self.old_deployment_version.image_version:
1270
1280
  extra_rollback_args = (
@@ -1843,7 +1853,7 @@ async def wait_for_deployment(
1843
1853
  system_paasta_config.get_mark_for_deployment_default_time_before_first_diagnosis()
1844
1854
  )
1845
1855
 
1846
- with progressbar.ProgressBar(maxval=total_instances) as bar:
1856
+ with progressbar.ProgressBar(max_value=total_instances) as bar:
1847
1857
  instance_done_futures = []
1848
1858
  with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
1849
1859
  for cluster, instance_configs in instance_configs_per_cluster.items():
@@ -44,9 +44,10 @@ def add_subparser(subparsers) -> None:
44
44
  mesh_status_parser.add_argument(
45
45
  "-s",
46
46
  "--service",
47
- type=str,
48
47
  help="The name of the service you wish to inspect",
49
48
  required=True,
49
+ # strip any potential trailing / for folks tab-completing directories
50
+ type=lambda x: x.rstrip("/"),
50
51
  ).completer = lazy_choices_completer(list_services)
51
52
  mesh_status_parser.add_argument(
52
53
  "-i",
@@ -62,6 +62,8 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
62
62
  help='Name of service for which you wish to upload a docker image. Leading "services-", '
63
63
  "as included in a Jenkins job name, will be stripped.",
64
64
  required=True,
65
+ # strip any potential trailing / for folks tab-completing directories
66
+ type=lambda x: x.rstrip("/"),
65
67
  )
66
68
  list_parser.add_argument(
67
69
  "-c",
@@ -295,11 +295,20 @@ def paasta_remote_run_stop(
295
295
 
296
296
 
297
297
  def add_common_args_to_parser(parser: argparse.ArgumentParser):
298
+ def _validate_service_name(name: str) -> str:
299
+ # remove trailing slash if present for folks tab-completing directories
300
+ if name.rstrip("/") not in _list_services_and_toolboxes():
301
+ raise ValueError(f"{name} is not a known service name")
302
+ return name
303
+
298
304
  service_arg = parser.add_argument(
299
305
  "-s",
300
306
  "--service",
301
307
  help="The name of the service you wish to inspect. Required.",
302
308
  required=True,
309
+ # not using `choices` for validation to avoid the help text
310
+ # for the command being incredibly large
311
+ type=_validate_service_name,
303
312
  )
304
313
  service_arg.completer = lazy_choices_completer(_list_services_and_toolboxes) # type: ignore
305
314
  instance_or_toolbox = parser.add_mutually_exclusive_group()
@@ -323,6 +332,7 @@ def add_common_args_to_parser(parser: argparse.ArgumentParser):
323
332
  "--cluster",
324
333
  help="The name of the cluster you wish to run your task on. Required.",
325
334
  required=True,
335
+ choices=list_clusters(),
326
336
  )
327
337
  cluster_arg.completer = lazy_choices_completer(list_clusters) # type: ignore
328
338
 
@@ -107,7 +107,11 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
107
107
  )
108
108
 
109
109
  arg_service = list_parser.add_argument(
110
- "-s", "--service", help='Name of the service to rollback (e.g. "service1")'
110
+ "-s",
111
+ "--service",
112
+ help='Name of the service to rollback (e.g. "service1")',
113
+ # strip any potential trailing / for folks tab-completing directories
114
+ type=lambda x: x.rstrip("/"),
111
115
  )
112
116
  arg_service.completer = lazy_choices_completer(list_services) # type: ignore
113
117
  list_parser.add_argument(
@@ -212,7 +212,7 @@ def _add_vault_auth_args(parser: argparse.ArgumentParser):
212
212
  dest="vault_auth_method",
213
213
  required=False,
214
214
  default="token",
215
- choices=["token", "ldap"],
215
+ choices=["token", "okta"],
216
216
  )
217
217
  parser.add_argument(
218
218
  "--vault-token-file",
@@ -246,6 +246,8 @@ def _add_common_args(parser: argparse.ArgumentParser, allow_shared: bool = True)
246
246
  "--service",
247
247
  required=not allow_shared,
248
248
  help="The name of the service on which you wish to act",
249
+ # strip any potential trailing / for folks tab-completing directories
250
+ type=lambda x: x.rstrip("/"),
249
251
  ).completer = lazy_choices_completer(list_services)
250
252
 
251
253
  if allow_shared:
@@ -456,7 +458,7 @@ def paasta_secret(args):
456
458
  "vault_token_file": args.vault_token_file,
457
459
  # best solution so far is to change the below string to "token",
458
460
  # so that token file is picked up from argparse
459
- "vault_auth_method": "ldap", # must have LDAP to get 2FA push for prod
461
+ "vault_auth_method": "okta", # must use Okta to get 2FA push
460
462
  },
461
463
  )
462
464
  secret_provider.write_secret(
@@ -28,6 +28,8 @@ def add_subparser(subparsers):
28
28
  help='Name of service for which you wish to check. Leading "services-", as included in a '
29
29
  "Jenkins job name, will be stripped.",
30
30
  required=True,
31
+ # strip any potential trailing / for folks tab-completing directories
32
+ type=lambda x: x.rstrip("/"),
31
33
  )
32
34
  list_parser.add_argument(
33
35
  "-c", "--commit", help="Git sha of the image to check", required=True
@@ -238,6 +238,8 @@ def add_subparser(subparsers):
238
238
  "--service",
239
239
  help="The name of the service from which the Spark image is built.",
240
240
  default=DEFAULT_SPARK_SERVICE,
241
+ # strip any potential trailing / for folks tab-completing directories
242
+ type=lambda x: x.rstrip("/"),
241
243
  ).completer = lazy_choices_completer(list_services)
242
244
 
243
245
  list_parser.add_argument(
@@ -897,6 +899,8 @@ def configure_and_run_docker_container(
897
899
  )
898
900
  ) # type:ignore
899
901
  environment.update(extra_driver_envs)
902
+ if "jupyter-lab" == args.cmd:
903
+ environment["SPARK_DRIVER_TYPE"] = "jupyter"
900
904
 
901
905
  if args.use_service_auth_token:
902
906
  environment["YELP_SVC_AUTHZ_TOKEN"] = get_service_auth_token()
@@ -203,7 +203,11 @@ def add_subparser(
203
203
 
204
204
  def add_instance_filter_arguments(status_parser, verb: str = "inspect") -> None:
205
205
  status_parser.add_argument(
206
- "-s", "--service", help=f"The name of the service you wish to {verb}"
206
+ "-s",
207
+ "--service",
208
+ help=f"The name of the service you wish to {verb}",
209
+ # strip any potential trailing / for folks tab-completing directories
210
+ type=lambda x: x.rstrip("/"),
207
211
  ).completer = lazy_choices_completer(list_services)
208
212
  status_parser.add_argument(
209
213
  "-c",
@@ -891,7 +895,7 @@ def _print_flink_status_from_job_manager(
891
895
 
892
896
  # Print Flink Costs Link
893
897
  output.append(
894
- f" Flink Cost: https://splunk.yelpcorp.com/en-US/app/yelp_computeinfra/paasta_service_utilization?form.service={service}&form.field1.earliest=-30d%40d&form.field1.latest=now&form.instance={instance}&form.cluster={cluster}"
898
+ f" Flink Cost: https://app.cloudzero.com/explorer?activeCostType=invoiced_amortized_cost&partitions=costcontext%3AResource%20Summary&dateRange=Last%2030%20Days&costcontext%3AKube%20Paasta%20Cluster={cluster}&costcontext%3APaasta%20Instance={instance}&costcontext%3APaasta%20Service={service}&showRightFlyout=filters"
895
899
  )
896
900
 
897
901
  output.append(f"{OUTPUT_HORIZONTAL_RULE}")
@@ -917,11 +921,13 @@ def _print_flink_status_from_job_manager(
917
921
  else f"{pod_evicted_count}"
918
922
  )
919
923
 
924
+ pods_total_count = pod_running_count + pod_evicted_count + pod_other_count
920
925
  output.append(
921
926
  " Pods:"
922
927
  f" {pod_running_count} running,"
923
928
  f" {evicted} evicted,"
924
- f" {pod_other_count} other"
929
+ f" {pod_other_count} other,"
930
+ f" {pods_total_count} total"
925
931
  )
926
932
 
927
933
  if not should_job_info_be_shown(status["state"]):
@@ -944,12 +950,19 @@ def _print_flink_status_from_job_manager(
944
950
  output.append(str(e))
945
951
  return 1
946
952
 
953
+ jobs_total_count = (
954
+ overview.jobs_running
955
+ + overview.jobs_finished
956
+ + overview.jobs_failed
957
+ + overview.jobs_cancelled
958
+ )
947
959
  output.append(
948
960
  " Jobs:"
949
961
  f" {overview.jobs_running} running,"
950
962
  f" {overview.jobs_finished} finished,"
951
963
  f" {overview.jobs_failed} failed,"
952
- f" {overview.jobs_cancelled} cancelled"
964
+ f" {overview.jobs_cancelled} cancelled,"
965
+ f" {jobs_total_count} total"
953
966
  )
954
967
  output.append(
955
968
  " "
@@ -1313,7 +1326,9 @@ def get_version_table_entry(
1313
1326
  for state in ReplicaState
1314
1327
  if state in replica_state_counts
1315
1328
  ]
1316
- entry.append(f" Replica States: {' / '.join(replica_state_display)}")
1329
+ entry.append(
1330
+ f" Replica States: {' / '.join(replica_state_display)} / {replica_state_counts.total()} total"
1331
+ )
1317
1332
  if not verbose:
1318
1333
  unhealthy_replicas = [
1319
1334
  (state, pod) for state, pod in replica_states if state.is_unhealthy()
@@ -1321,13 +1336,23 @@ def get_version_table_entry(
1321
1336
  if unhealthy_replicas:
1322
1337
  entry.append(" Unhealthy Replicas:")
1323
1338
  replica_table = create_replica_table(
1324
- unhealthy_replicas, service, instance, cluster, verbose
1339
+ unhealthy_replicas,
1340
+ service,
1341
+ instance,
1342
+ cluster,
1343
+ version.container_port,
1344
+ verbose,
1325
1345
  )
1326
1346
  for line in replica_table:
1327
1347
  entry.append(f" {line}")
1328
1348
  else:
1329
1349
  replica_table = create_replica_table(
1330
- replica_states, service, instance, cluster, verbose
1350
+ replica_states,
1351
+ service,
1352
+ instance,
1353
+ cluster,
1354
+ version.container_port,
1355
+ verbose,
1331
1356
  )
1332
1357
  for line in replica_table:
1333
1358
  entry.append(f" {line}")
@@ -1467,6 +1492,7 @@ def create_replica_table(
1467
1492
  service: str,
1468
1493
  instance: str,
1469
1494
  cluster: str,
1495
+ container_port: int,
1470
1496
  verbose: int = 0,
1471
1497
  ) -> List[str]:
1472
1498
  header = ["ID", "IP/Port", "Host deployed to", "Started at what localtime", "State"]
@@ -1474,9 +1500,10 @@ def create_replica_table(
1474
1500
  for state, pod in pods:
1475
1501
  start_datetime = datetime.fromtimestamp(pod.create_timestamp)
1476
1502
  humanized_start_time = humanize.naturaltime(start_datetime)
1503
+
1477
1504
  row = [
1478
1505
  pod.name,
1479
- f"{pod.ip}:8888" if pod.ip else "None",
1506
+ f"{pod.ip}:{container_port}" if pod.ip else "None",
1480
1507
  pod.host or "None",
1481
1508
  humanized_start_time,
1482
1509
  state.formatted_message,