paasta-tools 1.21.3__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.
- k8s_itests/__init__.py +0 -0
- k8s_itests/test_autoscaling.py +23 -0
- k8s_itests/utils.py +38 -0
- paasta_tools/__init__.py +20 -0
- paasta_tools/adhoc_tools.py +142 -0
- paasta_tools/api/__init__.py +13 -0
- paasta_tools/api/api.py +330 -0
- paasta_tools/api/api_docs/swagger.json +2323 -0
- paasta_tools/api/client.py +106 -0
- paasta_tools/api/settings.py +33 -0
- paasta_tools/api/tweens/__init__.py +6 -0
- paasta_tools/api/tweens/auth.py +125 -0
- paasta_tools/api/tweens/profiling.py +108 -0
- paasta_tools/api/tweens/request_logger.py +124 -0
- paasta_tools/api/views/__init__.py +13 -0
- paasta_tools/api/views/autoscaler.py +100 -0
- paasta_tools/api/views/exception.py +45 -0
- paasta_tools/api/views/flink.py +73 -0
- paasta_tools/api/views/instance.py +395 -0
- paasta_tools/api/views/pause_autoscaler.py +71 -0
- paasta_tools/api/views/remote_run.py +113 -0
- paasta_tools/api/views/resources.py +76 -0
- paasta_tools/api/views/service.py +35 -0
- paasta_tools/api/views/version.py +25 -0
- paasta_tools/apply_external_resources.py +79 -0
- paasta_tools/async_utils.py +109 -0
- paasta_tools/autoscaling/__init__.py +0 -0
- paasta_tools/autoscaling/autoscaling_service_lib.py +57 -0
- paasta_tools/autoscaling/forecasting.py +106 -0
- paasta_tools/autoscaling/max_all_k8s_services.py +41 -0
- paasta_tools/autoscaling/pause_service_autoscaler.py +77 -0
- paasta_tools/autoscaling/utils.py +52 -0
- paasta_tools/bounce_lib.py +184 -0
- paasta_tools/broadcast_log_to_services.py +62 -0
- paasta_tools/cassandracluster_tools.py +210 -0
- paasta_tools/check_autoscaler_max_instances.py +212 -0
- paasta_tools/check_cassandracluster_services_replication.py +35 -0
- paasta_tools/check_flink_services_health.py +203 -0
- paasta_tools/check_kubernetes_api.py +57 -0
- paasta_tools/check_kubernetes_services_replication.py +141 -0
- paasta_tools/check_oom_events.py +244 -0
- paasta_tools/check_services_replication_tools.py +324 -0
- paasta_tools/check_spark_jobs.py +234 -0
- paasta_tools/cleanup_kubernetes_cr.py +138 -0
- paasta_tools/cleanup_kubernetes_crd.py +145 -0
- paasta_tools/cleanup_kubernetes_jobs.py +344 -0
- paasta_tools/cleanup_tron_namespaces.py +96 -0
- paasta_tools/cli/__init__.py +13 -0
- paasta_tools/cli/authentication.py +85 -0
- paasta_tools/cli/cli.py +260 -0
- paasta_tools/cli/cmds/__init__.py +13 -0
- paasta_tools/cli/cmds/autoscale.py +143 -0
- paasta_tools/cli/cmds/check.py +334 -0
- paasta_tools/cli/cmds/cook_image.py +147 -0
- paasta_tools/cli/cmds/get_docker_image.py +76 -0
- paasta_tools/cli/cmds/get_image_version.py +172 -0
- paasta_tools/cli/cmds/get_latest_deployment.py +93 -0
- paasta_tools/cli/cmds/info.py +155 -0
- paasta_tools/cli/cmds/itest.py +117 -0
- paasta_tools/cli/cmds/list.py +66 -0
- paasta_tools/cli/cmds/list_clusters.py +42 -0
- paasta_tools/cli/cmds/list_deploy_queue.py +171 -0
- paasta_tools/cli/cmds/list_namespaces.py +84 -0
- paasta_tools/cli/cmds/local_run.py +1396 -0
- paasta_tools/cli/cmds/logs.py +1601 -0
- paasta_tools/cli/cmds/mark_for_deployment.py +1988 -0
- paasta_tools/cli/cmds/mesh_status.py +174 -0
- paasta_tools/cli/cmds/pause_service_autoscaler.py +107 -0
- paasta_tools/cli/cmds/push_to_registry.py +275 -0
- paasta_tools/cli/cmds/remote_run.py +252 -0
- paasta_tools/cli/cmds/rollback.py +347 -0
- paasta_tools/cli/cmds/secret.py +549 -0
- paasta_tools/cli/cmds/security_check.py +59 -0
- paasta_tools/cli/cmds/spark_run.py +1400 -0
- paasta_tools/cli/cmds/start_stop_restart.py +401 -0
- paasta_tools/cli/cmds/status.py +2302 -0
- paasta_tools/cli/cmds/validate.py +1012 -0
- paasta_tools/cli/cmds/wait_for_deployment.py +275 -0
- paasta_tools/cli/fsm/__init__.py +13 -0
- paasta_tools/cli/fsm/autosuggest.py +82 -0
- paasta_tools/cli/fsm/template/README.md +8 -0
- paasta_tools/cli/fsm/template/cookiecutter.json +7 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/kubernetes-PROD.yaml +91 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/monitoring.yaml +20 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/service.yaml +8 -0
- paasta_tools/cli/fsm/template/{{cookiecutter.service}}/smartstack.yaml +6 -0
- paasta_tools/cli/fsm_cmd.py +121 -0
- paasta_tools/cli/paasta_tabcomplete.sh +23 -0
- paasta_tools/cli/schemas/adhoc_schema.json +199 -0
- paasta_tools/cli/schemas/autoscaling_schema.json +91 -0
- paasta_tools/cli/schemas/autotuned_defaults/cassandracluster_schema.json +37 -0
- paasta_tools/cli/schemas/autotuned_defaults/kubernetes_schema.json +89 -0
- paasta_tools/cli/schemas/deploy_schema.json +173 -0
- paasta_tools/cli/schemas/eks_schema.json +970 -0
- paasta_tools/cli/schemas/kubernetes_schema.json +970 -0
- paasta_tools/cli/schemas/rollback_schema.json +160 -0
- paasta_tools/cli/schemas/service_schema.json +25 -0
- paasta_tools/cli/schemas/smartstack_schema.json +322 -0
- paasta_tools/cli/schemas/tron_schema.json +699 -0
- paasta_tools/cli/utils.py +1118 -0
- paasta_tools/clusterman.py +21 -0
- paasta_tools/config_utils.py +385 -0
- paasta_tools/contrib/__init__.py +0 -0
- paasta_tools/contrib/bounce_log_latency_parser.py +68 -0
- paasta_tools/contrib/check_manual_oapi_changes.sh +24 -0
- paasta_tools/contrib/check_orphans.py +306 -0
- paasta_tools/contrib/create_dynamodb_table.py +35 -0
- paasta_tools/contrib/create_paasta_playground.py +105 -0
- paasta_tools/contrib/emit_allocated_cpu_metrics.py +50 -0
- paasta_tools/contrib/get_running_task_allocation.py +346 -0
- paasta_tools/contrib/habitat_fixer.py +86 -0
- paasta_tools/contrib/ide_helper.py +316 -0
- paasta_tools/contrib/is_pod_healthy_in_proxy.py +139 -0
- paasta_tools/contrib/is_pod_healthy_in_smartstack.py +50 -0
- paasta_tools/contrib/kill_bad_containers.py +109 -0
- paasta_tools/contrib/mass-deploy-tag.sh +44 -0
- paasta_tools/contrib/mock_patch_checker.py +86 -0
- paasta_tools/contrib/paasta_update_soa_memcpu.py +520 -0
- paasta_tools/contrib/render_template.py +129 -0
- paasta_tools/contrib/rightsizer_soaconfigs_update.py +348 -0
- paasta_tools/contrib/service_shard_remove.py +157 -0
- paasta_tools/contrib/service_shard_update.py +373 -0
- paasta_tools/contrib/shared_ip_check.py +77 -0
- paasta_tools/contrib/timeouts_metrics_prom.py +64 -0
- paasta_tools/delete_kubernetes_deployments.py +89 -0
- paasta_tools/deployment_utils.py +44 -0
- paasta_tools/docker_wrapper.py +234 -0
- paasta_tools/docker_wrapper_imports.py +13 -0
- paasta_tools/drain_lib.py +351 -0
- paasta_tools/dump_locally_running_services.py +71 -0
- paasta_tools/eks_tools.py +119 -0
- paasta_tools/envoy_tools.py +373 -0
- paasta_tools/firewall.py +504 -0
- paasta_tools/firewall_logging.py +154 -0
- paasta_tools/firewall_update.py +172 -0
- paasta_tools/flink_tools.py +345 -0
- paasta_tools/flinkeks_tools.py +90 -0
- paasta_tools/frameworks/__init__.py +0 -0
- paasta_tools/frameworks/adhoc_scheduler.py +71 -0
- paasta_tools/frameworks/constraints.py +87 -0
- paasta_tools/frameworks/native_scheduler.py +652 -0
- paasta_tools/frameworks/native_service_config.py +301 -0
- paasta_tools/frameworks/task_store.py +245 -0
- paasta_tools/generate_all_deployments +9 -0
- paasta_tools/generate_authenticating_services.py +94 -0
- paasta_tools/generate_deployments_for_service.py +255 -0
- paasta_tools/generate_services_file.py +114 -0
- paasta_tools/generate_services_yaml.py +30 -0
- paasta_tools/hacheck.py +76 -0
- paasta_tools/instance/__init__.py +0 -0
- paasta_tools/instance/hpa_metrics_parser.py +122 -0
- paasta_tools/instance/kubernetes.py +1362 -0
- paasta_tools/iptables.py +240 -0
- paasta_tools/kafkacluster_tools.py +143 -0
- paasta_tools/kubernetes/__init__.py +0 -0
- paasta_tools/kubernetes/application/__init__.py +0 -0
- paasta_tools/kubernetes/application/controller_wrappers.py +476 -0
- paasta_tools/kubernetes/application/tools.py +90 -0
- paasta_tools/kubernetes/bin/__init__.py +0 -0
- paasta_tools/kubernetes/bin/kubernetes_remove_evicted_pods.py +164 -0
- paasta_tools/kubernetes/bin/paasta_cleanup_remote_run_resources.py +135 -0
- paasta_tools/kubernetes/bin/paasta_cleanup_stale_nodes.py +181 -0
- paasta_tools/kubernetes/bin/paasta_secrets_sync.py +758 -0
- paasta_tools/kubernetes/remote_run.py +558 -0
- paasta_tools/kubernetes_tools.py +4679 -0
- paasta_tools/list_kubernetes_service_instances.py +128 -0
- paasta_tools/list_tron_namespaces.py +60 -0
- paasta_tools/long_running_service_tools.py +678 -0
- paasta_tools/mac_address.py +44 -0
- paasta_tools/marathon_dashboard.py +0 -0
- paasta_tools/mesos/__init__.py +0 -0
- paasta_tools/mesos/cfg.py +46 -0
- paasta_tools/mesos/cluster.py +60 -0
- paasta_tools/mesos/exceptions.py +59 -0
- paasta_tools/mesos/framework.py +77 -0
- paasta_tools/mesos/log.py +48 -0
- paasta_tools/mesos/master.py +306 -0
- paasta_tools/mesos/mesos_file.py +169 -0
- paasta_tools/mesos/parallel.py +52 -0
- paasta_tools/mesos/slave.py +115 -0
- paasta_tools/mesos/task.py +94 -0
- paasta_tools/mesos/util.py +69 -0
- paasta_tools/mesos/zookeeper.py +37 -0
- paasta_tools/mesos_maintenance.py +848 -0
- paasta_tools/mesos_tools.py +1051 -0
- paasta_tools/metrics/__init__.py +0 -0
- paasta_tools/metrics/metastatus_lib.py +1110 -0
- paasta_tools/metrics/metrics_lib.py +217 -0
- paasta_tools/monitoring/__init__.py +13 -0
- paasta_tools/monitoring/check_k8s_api_performance.py +110 -0
- paasta_tools/monitoring_tools.py +652 -0
- paasta_tools/monkrelaycluster_tools.py +146 -0
- paasta_tools/nrtsearchservice_tools.py +143 -0
- paasta_tools/nrtsearchserviceeks_tools.py +68 -0
- paasta_tools/oom_logger.py +321 -0
- paasta_tools/paasta_deploy_tron_jobs +3 -0
- paasta_tools/paasta_execute_docker_command.py +123 -0
- paasta_tools/paasta_native_serviceinit.py +21 -0
- paasta_tools/paasta_service_config_loader.py +201 -0
- paasta_tools/paastaapi/__init__.py +29 -0
- paasta_tools/paastaapi/api/__init__.py +3 -0
- paasta_tools/paastaapi/api/autoscaler_api.py +302 -0
- paasta_tools/paastaapi/api/default_api.py +569 -0
- paasta_tools/paastaapi/api/remote_run_api.py +604 -0
- paasta_tools/paastaapi/api/resources_api.py +157 -0
- paasta_tools/paastaapi/api/service_api.py +1736 -0
- paasta_tools/paastaapi/api_client.py +818 -0
- paasta_tools/paastaapi/apis/__init__.py +22 -0
- paasta_tools/paastaapi/configuration.py +455 -0
- paasta_tools/paastaapi/exceptions.py +137 -0
- paasta_tools/paastaapi/model/__init__.py +5 -0
- paasta_tools/paastaapi/model/adhoc_launch_history.py +176 -0
- paasta_tools/paastaapi/model/autoscaler_count_msg.py +176 -0
- paasta_tools/paastaapi/model/deploy_queue.py +178 -0
- paasta_tools/paastaapi/model/deploy_queue_service_instance.py +194 -0
- paasta_tools/paastaapi/model/envoy_backend.py +185 -0
- paasta_tools/paastaapi/model/envoy_location.py +184 -0
- paasta_tools/paastaapi/model/envoy_status.py +181 -0
- paasta_tools/paastaapi/model/flink_cluster_overview.py +188 -0
- paasta_tools/paastaapi/model/flink_config.py +173 -0
- paasta_tools/paastaapi/model/flink_job.py +186 -0
- paasta_tools/paastaapi/model/flink_job_details.py +192 -0
- paasta_tools/paastaapi/model/flink_jobs.py +175 -0
- paasta_tools/paastaapi/model/float_and_error.py +173 -0
- paasta_tools/paastaapi/model/hpa_metric.py +176 -0
- paasta_tools/paastaapi/model/inline_object.py +170 -0
- paasta_tools/paastaapi/model/inline_response200.py +170 -0
- paasta_tools/paastaapi/model/inline_response2001.py +170 -0
- paasta_tools/paastaapi/model/instance_bounce_status.py +200 -0
- paasta_tools/paastaapi/model/instance_mesh_status.py +186 -0
- paasta_tools/paastaapi/model/instance_status.py +220 -0
- paasta_tools/paastaapi/model/instance_status_adhoc.py +187 -0
- paasta_tools/paastaapi/model/instance_status_cassandracluster.py +173 -0
- paasta_tools/paastaapi/model/instance_status_flink.py +173 -0
- paasta_tools/paastaapi/model/instance_status_kafkacluster.py +173 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes.py +263 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes_autoscaling_status.py +187 -0
- paasta_tools/paastaapi/model/instance_status_kubernetes_v2.py +197 -0
- paasta_tools/paastaapi/model/instance_status_tron.py +204 -0
- paasta_tools/paastaapi/model/instance_tasks.py +182 -0
- paasta_tools/paastaapi/model/integer_and_error.py +173 -0
- paasta_tools/paastaapi/model/kubernetes_container.py +178 -0
- paasta_tools/paastaapi/model/kubernetes_container_v2.py +219 -0
- paasta_tools/paastaapi/model/kubernetes_healthcheck.py +176 -0
- paasta_tools/paastaapi/model/kubernetes_pod.py +201 -0
- paasta_tools/paastaapi/model/kubernetes_pod_event.py +176 -0
- paasta_tools/paastaapi/model/kubernetes_pod_v2.py +213 -0
- paasta_tools/paastaapi/model/kubernetes_replica_set.py +185 -0
- paasta_tools/paastaapi/model/kubernetes_version.py +202 -0
- paasta_tools/paastaapi/model/remote_run_outcome.py +189 -0
- paasta_tools/paastaapi/model/remote_run_start.py +185 -0
- paasta_tools/paastaapi/model/remote_run_stop.py +176 -0
- paasta_tools/paastaapi/model/remote_run_token.py +173 -0
- paasta_tools/paastaapi/model/resource.py +187 -0
- paasta_tools/paastaapi/model/resource_item.py +187 -0
- paasta_tools/paastaapi/model/resource_value.py +176 -0
- paasta_tools/paastaapi/model/smartstack_backend.py +191 -0
- paasta_tools/paastaapi/model/smartstack_location.py +181 -0
- paasta_tools/paastaapi/model/smartstack_status.py +181 -0
- paasta_tools/paastaapi/model/task_tail_lines.py +176 -0
- paasta_tools/paastaapi/model_utils.py +1879 -0
- paasta_tools/paastaapi/models/__init__.py +62 -0
- paasta_tools/paastaapi/rest.py +287 -0
- paasta_tools/prune_completed_pods.py +220 -0
- paasta_tools/puppet_service_tools.py +59 -0
- paasta_tools/py.typed +1 -0
- paasta_tools/remote_git.py +127 -0
- paasta_tools/run-paasta-api-in-dev-mode.py +57 -0
- paasta_tools/run-paasta-api-playground.py +51 -0
- paasta_tools/secret_providers/__init__.py +66 -0
- paasta_tools/secret_providers/vault.py +214 -0
- paasta_tools/secret_tools.py +277 -0
- paasta_tools/setup_istio_mesh.py +353 -0
- paasta_tools/setup_kubernetes_cr.py +412 -0
- paasta_tools/setup_kubernetes_crd.py +138 -0
- paasta_tools/setup_kubernetes_internal_crd.py +154 -0
- paasta_tools/setup_kubernetes_job.py +353 -0
- paasta_tools/setup_prometheus_adapter_config.py +1028 -0
- paasta_tools/setup_tron_namespace.py +248 -0
- paasta_tools/slack.py +75 -0
- paasta_tools/smartstack_tools.py +676 -0
- paasta_tools/spark_tools.py +283 -0
- paasta_tools/synapse_srv_namespaces_fact.py +42 -0
- paasta_tools/tron/__init__.py +0 -0
- paasta_tools/tron/client.py +158 -0
- paasta_tools/tron/tron_command_context.py +194 -0
- paasta_tools/tron/tron_timeutils.py +101 -0
- paasta_tools/tron_tools.py +1448 -0
- paasta_tools/utils.py +4307 -0
- paasta_tools/yaml_tools.py +44 -0
- paasta_tools-1.21.3.data/scripts/apply_external_resources.py +79 -0
- paasta_tools-1.21.3.data/scripts/bounce_log_latency_parser.py +68 -0
- paasta_tools-1.21.3.data/scripts/check_autoscaler_max_instances.py +212 -0
- paasta_tools-1.21.3.data/scripts/check_cassandracluster_services_replication.py +35 -0
- paasta_tools-1.21.3.data/scripts/check_flink_services_health.py +203 -0
- paasta_tools-1.21.3.data/scripts/check_kubernetes_api.py +57 -0
- paasta_tools-1.21.3.data/scripts/check_kubernetes_services_replication.py +141 -0
- paasta_tools-1.21.3.data/scripts/check_manual_oapi_changes.sh +24 -0
- paasta_tools-1.21.3.data/scripts/check_oom_events.py +244 -0
- paasta_tools-1.21.3.data/scripts/check_orphans.py +306 -0
- paasta_tools-1.21.3.data/scripts/check_spark_jobs.py +234 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_cr.py +138 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_crd.py +145 -0
- paasta_tools-1.21.3.data/scripts/cleanup_kubernetes_jobs.py +344 -0
- paasta_tools-1.21.3.data/scripts/create_dynamodb_table.py +35 -0
- paasta_tools-1.21.3.data/scripts/create_paasta_playground.py +105 -0
- paasta_tools-1.21.3.data/scripts/delete_kubernetes_deployments.py +89 -0
- paasta_tools-1.21.3.data/scripts/emit_allocated_cpu_metrics.py +50 -0
- paasta_tools-1.21.3.data/scripts/generate_all_deployments +9 -0
- paasta_tools-1.21.3.data/scripts/generate_authenticating_services.py +94 -0
- paasta_tools-1.21.3.data/scripts/generate_deployments_for_service.py +255 -0
- paasta_tools-1.21.3.data/scripts/generate_services_file.py +114 -0
- paasta_tools-1.21.3.data/scripts/generate_services_yaml.py +30 -0
- paasta_tools-1.21.3.data/scripts/get_running_task_allocation.py +346 -0
- paasta_tools-1.21.3.data/scripts/habitat_fixer.py +86 -0
- paasta_tools-1.21.3.data/scripts/ide_helper.py +316 -0
- paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_proxy.py +139 -0
- paasta_tools-1.21.3.data/scripts/is_pod_healthy_in_smartstack.py +50 -0
- paasta_tools-1.21.3.data/scripts/kill_bad_containers.py +109 -0
- paasta_tools-1.21.3.data/scripts/kubernetes_remove_evicted_pods.py +164 -0
- paasta_tools-1.21.3.data/scripts/mass-deploy-tag.sh +44 -0
- paasta_tools-1.21.3.data/scripts/mock_patch_checker.py +86 -0
- paasta_tools-1.21.3.data/scripts/paasta_cleanup_remote_run_resources.py +135 -0
- paasta_tools-1.21.3.data/scripts/paasta_cleanup_stale_nodes.py +181 -0
- paasta_tools-1.21.3.data/scripts/paasta_deploy_tron_jobs +3 -0
- paasta_tools-1.21.3.data/scripts/paasta_execute_docker_command.py +123 -0
- paasta_tools-1.21.3.data/scripts/paasta_secrets_sync.py +758 -0
- paasta_tools-1.21.3.data/scripts/paasta_tabcomplete.sh +23 -0
- paasta_tools-1.21.3.data/scripts/paasta_update_soa_memcpu.py +520 -0
- paasta_tools-1.21.3.data/scripts/render_template.py +129 -0
- paasta_tools-1.21.3.data/scripts/rightsizer_soaconfigs_update.py +348 -0
- paasta_tools-1.21.3.data/scripts/service_shard_remove.py +157 -0
- paasta_tools-1.21.3.data/scripts/service_shard_update.py +373 -0
- paasta_tools-1.21.3.data/scripts/setup_istio_mesh.py +353 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_cr.py +412 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_crd.py +138 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_internal_crd.py +154 -0
- paasta_tools-1.21.3.data/scripts/setup_kubernetes_job.py +353 -0
- paasta_tools-1.21.3.data/scripts/setup_prometheus_adapter_config.py +1028 -0
- paasta_tools-1.21.3.data/scripts/shared_ip_check.py +77 -0
- paasta_tools-1.21.3.data/scripts/synapse_srv_namespaces_fact.py +42 -0
- paasta_tools-1.21.3.data/scripts/timeouts_metrics_prom.py +64 -0
- paasta_tools-1.21.3.dist-info/LICENSE +201 -0
- paasta_tools-1.21.3.dist-info/METADATA +74 -0
- paasta_tools-1.21.3.dist-info/RECORD +348 -0
- paasta_tools-1.21.3.dist-info/WHEEL +5 -0
- paasta_tools-1.21.3.dist-info/entry_points.txt +20 -0
- paasta_tools-1.21.3.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# Copyright 2015-2016 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 datetime
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import socket
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from os import execlpe
|
|
27
|
+
from random import randint
|
|
28
|
+
from typing import Optional
|
|
29
|
+
from urllib.parse import urlparse
|
|
30
|
+
|
|
31
|
+
import boto3
|
|
32
|
+
import requests
|
|
33
|
+
from docker import errors
|
|
34
|
+
from mypy_extensions import TypedDict
|
|
35
|
+
|
|
36
|
+
from paasta_tools.adhoc_tools import get_default_interactive_config
|
|
37
|
+
from paasta_tools.cli.authentication import get_service_auth_token
|
|
38
|
+
from paasta_tools.cli.authentication import get_sso_auth_token
|
|
39
|
+
from paasta_tools.cli.cmds.check import makefile_responds_to
|
|
40
|
+
from paasta_tools.cli.cmds.cook_image import paasta_cook_image
|
|
41
|
+
from paasta_tools.cli.utils import figure_out_service_name
|
|
42
|
+
from paasta_tools.cli.utils import get_instance_config
|
|
43
|
+
from paasta_tools.cli.utils import lazy_choices_completer
|
|
44
|
+
from paasta_tools.cli.utils import list_instances
|
|
45
|
+
from paasta_tools.cli.utils import pick_random_port
|
|
46
|
+
from paasta_tools.generate_deployments_for_service import build_docker_image_name
|
|
47
|
+
from paasta_tools.kubernetes_tools import get_kubernetes_secret_env_variables
|
|
48
|
+
from paasta_tools.kubernetes_tools import get_kubernetes_secret_volumes
|
|
49
|
+
from paasta_tools.kubernetes_tools import KUBE_CONFIG_USER_PATH
|
|
50
|
+
from paasta_tools.kubernetes_tools import KubeClient
|
|
51
|
+
from paasta_tools.long_running_service_tools import get_healthcheck_for_instance
|
|
52
|
+
from paasta_tools.paasta_execute_docker_command import execute_in_container
|
|
53
|
+
from paasta_tools.secret_tools import decrypt_secret_environment_variables
|
|
54
|
+
from paasta_tools.secret_tools import decrypt_secret_volumes
|
|
55
|
+
from paasta_tools.tron_tools import parse_time_variables
|
|
56
|
+
from paasta_tools.utils import _run
|
|
57
|
+
from paasta_tools.utils import DEFAULT_SOA_DIR
|
|
58
|
+
from paasta_tools.utils import get_docker_client
|
|
59
|
+
from paasta_tools.utils import get_possible_launched_by_user_variable_from_env
|
|
60
|
+
from paasta_tools.utils import get_username
|
|
61
|
+
from paasta_tools.utils import InstanceConfig
|
|
62
|
+
from paasta_tools.utils import is_secrets_for_teams_enabled
|
|
63
|
+
from paasta_tools.utils import list_clusters
|
|
64
|
+
from paasta_tools.utils import list_services
|
|
65
|
+
from paasta_tools.utils import load_system_paasta_config
|
|
66
|
+
from paasta_tools.utils import NoConfigurationForServiceError
|
|
67
|
+
from paasta_tools.utils import NoDeploymentsAvailable
|
|
68
|
+
from paasta_tools.utils import NoDockerImageError
|
|
69
|
+
from paasta_tools.utils import PaastaColors
|
|
70
|
+
from paasta_tools.utils import PaastaNotConfiguredError
|
|
71
|
+
from paasta_tools.utils import SystemPaastaConfig
|
|
72
|
+
from paasta_tools.utils import Timeout
|
|
73
|
+
from paasta_tools.utils import TimeoutError
|
|
74
|
+
from paasta_tools.utils import validate_service_instance
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AWSSessionCreds(TypedDict):
|
|
78
|
+
AWS_ACCESS_KEY_ID: str
|
|
79
|
+
AWS_SECRET_ACCESS_KEY: str
|
|
80
|
+
AWS_SESSION_TOKEN: str
|
|
81
|
+
AWS_SECURITY_TOKEN: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_date(date_string):
|
|
85
|
+
return datetime.datetime.strptime(date_string, "%Y-%m-%d")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def perform_http_healthcheck(url, timeout):
|
|
89
|
+
"""Returns true if healthcheck on url succeeds, false otherwise
|
|
90
|
+
|
|
91
|
+
:param url: the healthcheck url
|
|
92
|
+
:param timeout: timeout in seconds
|
|
93
|
+
:returns: True if healthcheck succeeds within number of seconds specified by timeout, false otherwise
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
with Timeout(seconds=timeout):
|
|
97
|
+
try:
|
|
98
|
+
res = requests.get(url, verify=False)
|
|
99
|
+
except requests.ConnectionError:
|
|
100
|
+
return (False, "http request failed: connection failed")
|
|
101
|
+
except TimeoutError:
|
|
102
|
+
return (False, "http request timed out after %d seconds" % timeout)
|
|
103
|
+
|
|
104
|
+
if "content-type" in res.headers and "," in res.headers["content-type"]:
|
|
105
|
+
print(
|
|
106
|
+
PaastaColors.yellow(
|
|
107
|
+
"Multiple content-type headers detected in response."
|
|
108
|
+
" The Mesos healthcheck system will treat this as a failure!"
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return (False, "http request succeeded, code %d" % res.status_code)
|
|
112
|
+
# check if response code is valid per https://mesosphere.github.io/marathon/docs/health-checks.html
|
|
113
|
+
elif res.status_code >= 200 and res.status_code < 400:
|
|
114
|
+
return (True, "http request succeeded, code %d" % res.status_code)
|
|
115
|
+
else:
|
|
116
|
+
return (False, "http request failed, code %s" % str(res.status_code))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def perform_tcp_healthcheck(url, timeout):
|
|
120
|
+
"""Returns true if successfully connects to host and port, false otherwise
|
|
121
|
+
|
|
122
|
+
:param url: the healthcheck url (in the form tcp://host:port)
|
|
123
|
+
:param timeout: timeout in seconds
|
|
124
|
+
:returns: True if healthcheck succeeds within number of seconds specified by timeout, false otherwise
|
|
125
|
+
"""
|
|
126
|
+
url_elem = urlparse(url)
|
|
127
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
128
|
+
sock.settimeout(timeout)
|
|
129
|
+
result = sock.connect_ex((url_elem.hostname, url_elem.port))
|
|
130
|
+
sock.close()
|
|
131
|
+
if result == 0:
|
|
132
|
+
return (True, "tcp connection succeeded")
|
|
133
|
+
else:
|
|
134
|
+
return (False, "%s (timeout %d seconds)" % (os.strerror(result), timeout))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def perform_cmd_healthcheck(docker_client, container_id, command, timeout):
|
|
138
|
+
"""Returns true if return code of command is 0 when executed inside container, false otherwise
|
|
139
|
+
|
|
140
|
+
:param docker_client: Docker client object
|
|
141
|
+
:param container_id: Docker container id
|
|
142
|
+
:param command: command to execute
|
|
143
|
+
:param timeout: timeout in seconds
|
|
144
|
+
:returns: True if command exits with return code 0, false otherwise
|
|
145
|
+
"""
|
|
146
|
+
(output, return_code) = execute_in_container(
|
|
147
|
+
docker_client, container_id, command, timeout
|
|
148
|
+
)
|
|
149
|
+
if return_code == 0:
|
|
150
|
+
return (True, output)
|
|
151
|
+
else:
|
|
152
|
+
return (False, output)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_healthcheck_on_container(
|
|
156
|
+
docker_client, container_id, healthcheck_mode, healthcheck_data, timeout
|
|
157
|
+
):
|
|
158
|
+
"""Performs healthcheck on a container
|
|
159
|
+
|
|
160
|
+
:param container_id: Docker container id
|
|
161
|
+
:param healthcheck_mode: one of 'http', 'https', 'tcp', or 'cmd'
|
|
162
|
+
:param healthcheck_data: a URL when healthcheck_mode is 'http[s]' or 'tcp', a command if healthcheck_mode is 'cmd'
|
|
163
|
+
:param timeout: timeout in seconds for individual check
|
|
164
|
+
:returns: a tuple of (bool, output string)
|
|
165
|
+
"""
|
|
166
|
+
healthcheck_result = (False, "unknown")
|
|
167
|
+
if healthcheck_mode == "cmd":
|
|
168
|
+
healthcheck_result = perform_cmd_healthcheck(
|
|
169
|
+
docker_client, container_id, healthcheck_data, timeout
|
|
170
|
+
)
|
|
171
|
+
elif healthcheck_mode == "http" or healthcheck_mode == "https":
|
|
172
|
+
healthcheck_result = perform_http_healthcheck(healthcheck_data, timeout)
|
|
173
|
+
elif healthcheck_mode == "tcp":
|
|
174
|
+
healthcheck_result = perform_tcp_healthcheck(healthcheck_data, timeout)
|
|
175
|
+
else:
|
|
176
|
+
print(
|
|
177
|
+
PaastaColors.yellow(
|
|
178
|
+
"Healthcheck mode '%s' is not currently supported!" % healthcheck_mode
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
return healthcheck_result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def simulate_healthcheck_on_service(
|
|
186
|
+
instance_config,
|
|
187
|
+
docker_client,
|
|
188
|
+
container_id,
|
|
189
|
+
healthcheck_mode,
|
|
190
|
+
healthcheck_data,
|
|
191
|
+
healthcheck_enabled,
|
|
192
|
+
):
|
|
193
|
+
"""Simulates Marathon-style healthcheck on given service if healthcheck is enabled
|
|
194
|
+
|
|
195
|
+
:param instance_config: service manifest
|
|
196
|
+
:param docker_client: Docker client object
|
|
197
|
+
:param container_id: Docker container id
|
|
198
|
+
:param healthcheck_data: tuple url to healthcheck
|
|
199
|
+
:param healthcheck_enabled: boolean
|
|
200
|
+
:returns: healthcheck_passed: boolean
|
|
201
|
+
"""
|
|
202
|
+
healthcheck_link = PaastaColors.cyan(healthcheck_data)
|
|
203
|
+
if healthcheck_enabled:
|
|
204
|
+
grace_period = instance_config.get_healthcheck_grace_period_seconds()
|
|
205
|
+
timeout = instance_config.get_healthcheck_timeout_seconds()
|
|
206
|
+
interval = instance_config.get_healthcheck_interval_seconds()
|
|
207
|
+
max_failures = instance_config.get_healthcheck_max_consecutive_failures()
|
|
208
|
+
|
|
209
|
+
print(
|
|
210
|
+
"\nStarting health check via %s (waiting %s seconds before "
|
|
211
|
+
"considering failures due to grace period):"
|
|
212
|
+
% (healthcheck_link, grace_period)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# silently start performing health checks until grace period ends or first check succeeds
|
|
216
|
+
graceperiod_end_time = time.time() + grace_period
|
|
217
|
+
after_grace_period_attempts = 0
|
|
218
|
+
healthchecking = True
|
|
219
|
+
|
|
220
|
+
def _stream_docker_logs(container_id, generator):
|
|
221
|
+
while healthchecking:
|
|
222
|
+
try:
|
|
223
|
+
# the generator will block until another log line is available
|
|
224
|
+
log_line = next(generator).decode("utf-8").rstrip("\n")
|
|
225
|
+
if healthchecking:
|
|
226
|
+
print(f"container [{container_id[:12]}]: {log_line}")
|
|
227
|
+
else:
|
|
228
|
+
# stop streaming at first opportunity, since generator.close()
|
|
229
|
+
# cant be used until the container is dead
|
|
230
|
+
break
|
|
231
|
+
except StopIteration: # natural end of logs
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
docker_logs_generator = docker_client.logs(
|
|
235
|
+
container_id, stderr=True, stream=True
|
|
236
|
+
)
|
|
237
|
+
threading.Thread(
|
|
238
|
+
target=_stream_docker_logs,
|
|
239
|
+
daemon=True,
|
|
240
|
+
args=(container_id, docker_logs_generator),
|
|
241
|
+
).start()
|
|
242
|
+
|
|
243
|
+
while True:
|
|
244
|
+
# First inspect the container for early exits
|
|
245
|
+
container_state = docker_client.inspect_container(container_id)
|
|
246
|
+
if not container_state["State"]["Running"]:
|
|
247
|
+
print(
|
|
248
|
+
PaastaColors.red(
|
|
249
|
+
"Container exited with code {}".format(
|
|
250
|
+
container_state["State"]["ExitCode"]
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
healthcheck_passed = False
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
healthcheck_passed, healthcheck_output = run_healthcheck_on_container(
|
|
258
|
+
docker_client, container_id, healthcheck_mode, healthcheck_data, timeout
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Yay, we passed the healthcheck
|
|
262
|
+
if healthcheck_passed:
|
|
263
|
+
print(
|
|
264
|
+
"{}'{}' (via {})".format(
|
|
265
|
+
PaastaColors.green("Healthcheck succeeded!: "),
|
|
266
|
+
healthcheck_output,
|
|
267
|
+
healthcheck_link,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
# Otherwise, print why we failed
|
|
273
|
+
if time.time() < graceperiod_end_time:
|
|
274
|
+
color = PaastaColors.grey
|
|
275
|
+
msg = "(disregarded due to grace period)"
|
|
276
|
+
extra_msg = f" (via: {healthcheck_link}. Output: {healthcheck_output})"
|
|
277
|
+
else:
|
|
278
|
+
# If we've exceeded the grace period, we start incrementing attempts
|
|
279
|
+
after_grace_period_attempts += 1
|
|
280
|
+
color = PaastaColors.red
|
|
281
|
+
msg = "(Attempt {} of {})".format(
|
|
282
|
+
after_grace_period_attempts, max_failures
|
|
283
|
+
)
|
|
284
|
+
extra_msg = f" (via: {healthcheck_link}. Output: {healthcheck_output})"
|
|
285
|
+
|
|
286
|
+
print("{}{}".format(color(f"Healthcheck failed! {msg}"), extra_msg))
|
|
287
|
+
|
|
288
|
+
if after_grace_period_attempts == max_failures:
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
time.sleep(interval)
|
|
292
|
+
healthchecking = False # end docker logs stream
|
|
293
|
+
else:
|
|
294
|
+
print(
|
|
295
|
+
"\nPaaSTA would have healthchecked your service via\n%s" % healthcheck_link
|
|
296
|
+
)
|
|
297
|
+
healthcheck_passed = True
|
|
298
|
+
return healthcheck_passed
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def read_local_dockerfile_lines():
|
|
302
|
+
dockerfile = os.path.join(os.getcwd(), "Dockerfile")
|
|
303
|
+
return open(dockerfile).readlines()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def add_subparser(subparsers):
|
|
307
|
+
list_parser = subparsers.add_parser(
|
|
308
|
+
"local-run",
|
|
309
|
+
help="Run service's Docker image locally",
|
|
310
|
+
description=(
|
|
311
|
+
"'paasta local-run' is useful for simulating how a PaaSTA service would be "
|
|
312
|
+
"executed on a real cluster. It analyzes the local soa-configs and constructs "
|
|
313
|
+
"a 'docker run' invocation to match. This is useful as a type of end-to-end "
|
|
314
|
+
"test, ensuring that a service will work inside the docker container as expected. "
|
|
315
|
+
"Additionally, 'local-run' can healthcheck a service per the configured healthcheck.\n\n"
|
|
316
|
+
"Alternatively, 'local-run' can be used with --pull, which will pull the currently "
|
|
317
|
+
"deployed docker image and use it, instead of building one."
|
|
318
|
+
),
|
|
319
|
+
epilog=(
|
|
320
|
+
"Note: 'paasta local-run' uses docker commands, which may require elevated privileges "
|
|
321
|
+
"to run (sudo)."
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
list_parser.add_argument(
|
|
325
|
+
"-s", "--service", help="The name of the service you wish to inspect"
|
|
326
|
+
).completer = lazy_choices_completer(list_services)
|
|
327
|
+
list_parser.add_argument(
|
|
328
|
+
"-c",
|
|
329
|
+
"--cluster",
|
|
330
|
+
help=(
|
|
331
|
+
"The name of the cluster you wish to simulate. "
|
|
332
|
+
"If omitted, uses the default cluster defined in the paasta local-run configs"
|
|
333
|
+
),
|
|
334
|
+
).completer = lazy_choices_completer(list_clusters)
|
|
335
|
+
list_parser.add_argument(
|
|
336
|
+
"-y",
|
|
337
|
+
"--yelpsoa-config-root",
|
|
338
|
+
dest="yelpsoa_config_root",
|
|
339
|
+
help="A directory from which yelpsoa-configs should be read from",
|
|
340
|
+
default=DEFAULT_SOA_DIR,
|
|
341
|
+
)
|
|
342
|
+
build_pull_group = list_parser.add_mutually_exclusive_group()
|
|
343
|
+
build_pull_group.add_argument(
|
|
344
|
+
"-b",
|
|
345
|
+
"--build",
|
|
346
|
+
help=(
|
|
347
|
+
"Build the docker image to run from scratch using the local Makefile's "
|
|
348
|
+
"'cook-image' target. Defaults to try to use the local Makefile if present."
|
|
349
|
+
),
|
|
350
|
+
action="store_const",
|
|
351
|
+
const="build",
|
|
352
|
+
dest="action",
|
|
353
|
+
)
|
|
354
|
+
build_pull_group.add_argument(
|
|
355
|
+
"-p",
|
|
356
|
+
"--pull",
|
|
357
|
+
help=(
|
|
358
|
+
"Pull the docker image marked for deployment from the Docker registry and "
|
|
359
|
+
"use that for the local-run. This is the opposite of --build."
|
|
360
|
+
),
|
|
361
|
+
action="store_const",
|
|
362
|
+
const="pull",
|
|
363
|
+
dest="action",
|
|
364
|
+
)
|
|
365
|
+
build_pull_group.add_argument(
|
|
366
|
+
"-d",
|
|
367
|
+
"--dry-run",
|
|
368
|
+
help="Shows the arguments supplied to docker as json.",
|
|
369
|
+
action="store_const",
|
|
370
|
+
const="dry_run",
|
|
371
|
+
dest="action",
|
|
372
|
+
)
|
|
373
|
+
build_pull_group.set_defaults(action="build")
|
|
374
|
+
list_parser.add_argument(
|
|
375
|
+
"--json-dict",
|
|
376
|
+
help="When running dry run, output the arguments as a json dict",
|
|
377
|
+
action="store_true",
|
|
378
|
+
dest="dry_run_json_dict",
|
|
379
|
+
)
|
|
380
|
+
list_parser.add_argument(
|
|
381
|
+
"-C",
|
|
382
|
+
"--cmd",
|
|
383
|
+
help=(
|
|
384
|
+
"Run Docker container with particular command, "
|
|
385
|
+
'for example: "bash". By default will use the command or args specified by the '
|
|
386
|
+
"soa-configs or what was specified in the Dockerfile"
|
|
387
|
+
),
|
|
388
|
+
required=False,
|
|
389
|
+
default=None,
|
|
390
|
+
)
|
|
391
|
+
list_parser.add_argument(
|
|
392
|
+
"-i",
|
|
393
|
+
"--instance",
|
|
394
|
+
help=(
|
|
395
|
+
"Simulate a docker run for a particular instance of the service, like 'main' or 'canary'. "
|
|
396
|
+
"NOTE: if you don't specify an instance, PaaSTA will run in interactive mode"
|
|
397
|
+
),
|
|
398
|
+
required=False,
|
|
399
|
+
default=None,
|
|
400
|
+
).completer = lazy_choices_completer(list_instances)
|
|
401
|
+
list_parser.add_argument(
|
|
402
|
+
"--date",
|
|
403
|
+
default=datetime.datetime.today().strftime("%Y-%m-%d"),
|
|
404
|
+
help="Date to use for interpolating date variables in a job. Defaults to use %(default)s.",
|
|
405
|
+
type=parse_date,
|
|
406
|
+
)
|
|
407
|
+
list_parser.add_argument(
|
|
408
|
+
"-v",
|
|
409
|
+
"--verbose",
|
|
410
|
+
help="Show Docker commands output",
|
|
411
|
+
action="store_true",
|
|
412
|
+
required=False,
|
|
413
|
+
default=True,
|
|
414
|
+
)
|
|
415
|
+
list_parser.add_argument(
|
|
416
|
+
"-I",
|
|
417
|
+
"--interactive",
|
|
418
|
+
help=(
|
|
419
|
+
'Run container in interactive mode. If interactive is set the default command will be "bash" '
|
|
420
|
+
'unless otherwise set by the "--cmd" flag'
|
|
421
|
+
),
|
|
422
|
+
action="store_true",
|
|
423
|
+
required=False,
|
|
424
|
+
default=False,
|
|
425
|
+
)
|
|
426
|
+
list_parser.add_argument(
|
|
427
|
+
"-k",
|
|
428
|
+
"--no-healthcheck",
|
|
429
|
+
help="Disable simulated healthcheck",
|
|
430
|
+
dest="healthcheck",
|
|
431
|
+
action="store_false",
|
|
432
|
+
required=False,
|
|
433
|
+
default=True,
|
|
434
|
+
)
|
|
435
|
+
list_parser.add_argument(
|
|
436
|
+
"-t",
|
|
437
|
+
"--healthcheck-only",
|
|
438
|
+
help="Terminates container after healthcheck (exits with status code 0 on success, 1 otherwise)",
|
|
439
|
+
dest="healthcheck_only",
|
|
440
|
+
action="store_true",
|
|
441
|
+
required=False,
|
|
442
|
+
default=False,
|
|
443
|
+
)
|
|
444
|
+
list_parser.add_argument(
|
|
445
|
+
"-o",
|
|
446
|
+
"--port",
|
|
447
|
+
help="Specify a port number to use. If not set, a random non-conflicting port will be found.",
|
|
448
|
+
type=int,
|
|
449
|
+
dest="user_port",
|
|
450
|
+
required=False,
|
|
451
|
+
default=False,
|
|
452
|
+
)
|
|
453
|
+
list_parser.add_argument(
|
|
454
|
+
"--vault-auth-method",
|
|
455
|
+
help="Override how we auth with vault, defaults to token if not present",
|
|
456
|
+
type=str,
|
|
457
|
+
dest="vault_auth_method",
|
|
458
|
+
required=False,
|
|
459
|
+
default="token",
|
|
460
|
+
choices=["token", "ldap"],
|
|
461
|
+
)
|
|
462
|
+
list_parser.add_argument(
|
|
463
|
+
"--vault-token-file",
|
|
464
|
+
help="Override vault token file, defaults to %(default)s",
|
|
465
|
+
type=str,
|
|
466
|
+
dest="vault_token_file",
|
|
467
|
+
required=False,
|
|
468
|
+
default="/var/spool/.paasta_vault_token",
|
|
469
|
+
)
|
|
470
|
+
list_parser.add_argument(
|
|
471
|
+
"--skip-secrets",
|
|
472
|
+
help="Skip decrypting secrets, useful if running non-interactively",
|
|
473
|
+
dest="skip_secrets",
|
|
474
|
+
required=False,
|
|
475
|
+
action="store_true",
|
|
476
|
+
default=False,
|
|
477
|
+
)
|
|
478
|
+
list_parser.add_argument(
|
|
479
|
+
"--assume-role-aws-account",
|
|
480
|
+
"--aws-account",
|
|
481
|
+
"-a",
|
|
482
|
+
help="Specify AWS account from which to source credentials",
|
|
483
|
+
)
|
|
484
|
+
list_parser.add_argument(
|
|
485
|
+
"--assume-role-arn",
|
|
486
|
+
help=(
|
|
487
|
+
"role ARN to assume before launching the service. "
|
|
488
|
+
"Example format: arn:aws:iam::01234567890:role/rolename"
|
|
489
|
+
),
|
|
490
|
+
type=str,
|
|
491
|
+
dest="assume_role_arn",
|
|
492
|
+
required=False,
|
|
493
|
+
default="",
|
|
494
|
+
)
|
|
495
|
+
list_parser.add_argument(
|
|
496
|
+
"--assume-pod-identity",
|
|
497
|
+
help="If pod identity is set via yelpsoa-configs, attempt to assume it",
|
|
498
|
+
action="store_true",
|
|
499
|
+
dest="assume_pod_identity",
|
|
500
|
+
required=False,
|
|
501
|
+
default=False,
|
|
502
|
+
)
|
|
503
|
+
list_parser.add_argument(
|
|
504
|
+
"--use-okta-role",
|
|
505
|
+
help="Call aws-okta and run the service within the context of the returned credentials",
|
|
506
|
+
dest="use_okta_role",
|
|
507
|
+
action="store_true",
|
|
508
|
+
required=False,
|
|
509
|
+
default=False,
|
|
510
|
+
)
|
|
511
|
+
list_parser.add_argument(
|
|
512
|
+
"--sha",
|
|
513
|
+
help=(
|
|
514
|
+
"SHA to run instead of the currently marked-for-deployment SHA. Ignored when used with --build."
|
|
515
|
+
" Must be a version that exists in the registry, i.e. it has been built by Jenkins."
|
|
516
|
+
),
|
|
517
|
+
type=str,
|
|
518
|
+
dest="sha",
|
|
519
|
+
required=False,
|
|
520
|
+
default=None,
|
|
521
|
+
)
|
|
522
|
+
list_parser.add_argument(
|
|
523
|
+
"--volume",
|
|
524
|
+
dest="volumes",
|
|
525
|
+
action="append",
|
|
526
|
+
type=str,
|
|
527
|
+
default=[],
|
|
528
|
+
required=False,
|
|
529
|
+
help=(
|
|
530
|
+
"Same as the -v / --volume parameter to docker run: hostPath:containerPath[:mode]"
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
service_auth_group = list_parser.add_mutually_exclusive_group()
|
|
534
|
+
service_auth_group.add_argument(
|
|
535
|
+
"--use-service-auth-token",
|
|
536
|
+
help=(
|
|
537
|
+
"Acquire service authentication token for the underlying instance,"
|
|
538
|
+
" and set it in the container environment"
|
|
539
|
+
),
|
|
540
|
+
action="store_true",
|
|
541
|
+
dest="use_service_auth_token",
|
|
542
|
+
required=False,
|
|
543
|
+
default=False,
|
|
544
|
+
)
|
|
545
|
+
service_auth_group.add_argument(
|
|
546
|
+
"--use-sso-service-auth-token",
|
|
547
|
+
help=(
|
|
548
|
+
"Acquire service authentication token from SSO provider,"
|
|
549
|
+
" and set it in the container environment"
|
|
550
|
+
),
|
|
551
|
+
action="store_true",
|
|
552
|
+
dest="use_sso_service_auth_token",
|
|
553
|
+
required=False,
|
|
554
|
+
default=False,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
list_parser.set_defaults(command=paasta_local_run)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def get_container_name():
|
|
561
|
+
return "paasta_local_run_{}_{}".format(get_username(), randint(1, 999999))
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def get_docker_run_cmd(
|
|
565
|
+
memory,
|
|
566
|
+
chosen_port,
|
|
567
|
+
container_port,
|
|
568
|
+
container_name,
|
|
569
|
+
volumes,
|
|
570
|
+
env,
|
|
571
|
+
interactive,
|
|
572
|
+
docker_hash,
|
|
573
|
+
command,
|
|
574
|
+
net,
|
|
575
|
+
docker_params,
|
|
576
|
+
detach,
|
|
577
|
+
):
|
|
578
|
+
cmd = ["paasta_docker_wrapper", "run"]
|
|
579
|
+
for k in env.keys():
|
|
580
|
+
cmd.append("--env")
|
|
581
|
+
cmd.append(f"{k}")
|
|
582
|
+
cmd.append("--memory=%dm" % memory)
|
|
583
|
+
for i in docker_params:
|
|
584
|
+
cmd.append(f"--{i['key']}={i['value']}")
|
|
585
|
+
if net == "bridge" and container_port is not None:
|
|
586
|
+
cmd.append("--publish=%d:%d" % (chosen_port, container_port))
|
|
587
|
+
elif net == "host":
|
|
588
|
+
cmd.append("--net=host")
|
|
589
|
+
cmd.append("--name=%s" % container_name)
|
|
590
|
+
for volume in volumes:
|
|
591
|
+
cmd.append("--volume=%s" % volume)
|
|
592
|
+
if interactive:
|
|
593
|
+
cmd.append("--interactive=true")
|
|
594
|
+
if sys.stdin.isatty():
|
|
595
|
+
cmd.append("--tty=true")
|
|
596
|
+
else:
|
|
597
|
+
if detach:
|
|
598
|
+
cmd.append("--detach=true")
|
|
599
|
+
cmd.append("%s" % docker_hash)
|
|
600
|
+
if command:
|
|
601
|
+
if isinstance(command, str):
|
|
602
|
+
cmd.extend(("sh", "-c", command))
|
|
603
|
+
else:
|
|
604
|
+
cmd.extend(command)
|
|
605
|
+
return cmd
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class LostContainerException(Exception):
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def docker_pull_image(docker_url):
|
|
613
|
+
"""Pull an image via ``docker pull``. Uses the actual pull command instead of the python
|
|
614
|
+
bindings due to the docker auth/registry transition. Once we are past Docker 1.6
|
|
615
|
+
we can use better credential management, but for now this function assumes the
|
|
616
|
+
user running the command has already been authorized for the registry"""
|
|
617
|
+
print(
|
|
618
|
+
"Please wait while the image (%s) is pulled (times out after 30m)..."
|
|
619
|
+
% docker_url,
|
|
620
|
+
file=sys.stderr,
|
|
621
|
+
)
|
|
622
|
+
with Timeout(
|
|
623
|
+
seconds=1800, error_message=f"Timed out pulling docker image from {docker_url}"
|
|
624
|
+
), open(os.devnull, mode="wb") as DEVNULL:
|
|
625
|
+
ret, _ = _run("docker pull %s" % docker_url, stream=True, stdin=DEVNULL)
|
|
626
|
+
if ret != 0:
|
|
627
|
+
print(
|
|
628
|
+
"\nPull failed. Are you authorized to run docker commands?",
|
|
629
|
+
file=sys.stderr,
|
|
630
|
+
)
|
|
631
|
+
sys.exit(ret)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def get_container_id(docker_client, container_name):
|
|
635
|
+
"""Use 'docker_client' to find the container we started, identifiable by
|
|
636
|
+
its 'container_name'. If we can't find the id, raise
|
|
637
|
+
LostContainerException.
|
|
638
|
+
"""
|
|
639
|
+
containers = docker_client.containers(all=False)
|
|
640
|
+
for container in containers:
|
|
641
|
+
if "/%s" % container_name in container.get("Names", []):
|
|
642
|
+
return container.get("Id")
|
|
643
|
+
raise LostContainerException(
|
|
644
|
+
"Can't find the container I just launched so I can't do anything else.\n"
|
|
645
|
+
"Try docker 'ps --all | grep %s' to see where it went.\n"
|
|
646
|
+
"Here were all the containers:\n"
|
|
647
|
+
"%s" % (container_name, containers)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _cleanup_container(docker_client, container_id):
|
|
652
|
+
if docker_client.inspect_container(container_id)["State"].get("OOMKilled", False):
|
|
653
|
+
print(
|
|
654
|
+
PaastaColors.red(
|
|
655
|
+
"Your service was killed by the OOM Killer!\n"
|
|
656
|
+
"You've exceeded the memory limit, try increasing the mem parameter in your soa_configs"
|
|
657
|
+
),
|
|
658
|
+
file=sys.stderr,
|
|
659
|
+
)
|
|
660
|
+
print("\nStopping and removing the old container %s..." % container_id)
|
|
661
|
+
print("(Please wait or you may leave an orphaned container.)")
|
|
662
|
+
try:
|
|
663
|
+
docker_client.stop(container_id)
|
|
664
|
+
docker_client.remove_container(container_id)
|
|
665
|
+
print("...done")
|
|
666
|
+
except errors.APIError:
|
|
667
|
+
print(
|
|
668
|
+
PaastaColors.yellow(
|
|
669
|
+
"Could not clean up container! You should stop and remove container '%s' manually."
|
|
670
|
+
% container_id
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def get_local_run_environment_vars(instance_config, port0, framework):
|
|
676
|
+
"""Returns a dictionary of environment variables to simulate what would be available to
|
|
677
|
+
a paasta service running in a container"""
|
|
678
|
+
hostname = socket.getfqdn()
|
|
679
|
+
docker_image = instance_config.get_docker_image()
|
|
680
|
+
if docker_image == "":
|
|
681
|
+
# In a local_run environment, the docker_image may not be available
|
|
682
|
+
# so we can fall-back to the injected DOCKER_TAG per the paasta contract
|
|
683
|
+
docker_image = os.environ["DOCKER_TAG"]
|
|
684
|
+
env = {
|
|
685
|
+
"HOST": hostname,
|
|
686
|
+
"PAASTA_DOCKER_IMAGE": docker_image,
|
|
687
|
+
"PAASTA_LAUNCHED_BY": get_possible_launched_by_user_variable_from_env(),
|
|
688
|
+
"PAASTA_HOST": hostname,
|
|
689
|
+
# Kubernetes instances remove PAASTA_CLUSTER, so we need to re-add it ourselves
|
|
690
|
+
"PAASTA_CLUSTER": instance_config.get_cluster(),
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return env
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def check_if_port_free(port):
|
|
697
|
+
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
698
|
+
try:
|
|
699
|
+
temp_socket.bind(("127.0.0.1", port))
|
|
700
|
+
except socket.error:
|
|
701
|
+
return False
|
|
702
|
+
finally:
|
|
703
|
+
temp_socket.close()
|
|
704
|
+
return True
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def resolve_aws_account_from_runtimeenv() -> str:
|
|
708
|
+
try:
|
|
709
|
+
with open("/nail/etc/runtimeenv") as runtimeenv_file:
|
|
710
|
+
runtimeenv = runtimeenv_file.read()
|
|
711
|
+
except FileNotFoundError:
|
|
712
|
+
print(
|
|
713
|
+
"Unable to determine environment for AWS account name. Using 'dev'",
|
|
714
|
+
file=sys.stderr,
|
|
715
|
+
)
|
|
716
|
+
runtimeenv = "dev"
|
|
717
|
+
|
|
718
|
+
runtimeenv_to_account_overrides = {
|
|
719
|
+
"stage": "dev",
|
|
720
|
+
"corp": "corpprod",
|
|
721
|
+
}
|
|
722
|
+
return runtimeenv_to_account_overrides.get(runtimeenv, runtimeenv)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def assume_aws_role(
|
|
726
|
+
instance_config: InstanceConfig,
|
|
727
|
+
service: str,
|
|
728
|
+
assume_role_arn: str,
|
|
729
|
+
assume_pod_identity: bool,
|
|
730
|
+
use_okta_role: bool,
|
|
731
|
+
aws_account: str,
|
|
732
|
+
) -> AWSSessionCreds:
|
|
733
|
+
"""Runs AWS cli to assume into the correct role, then extract and return the ENV variables from that session"""
|
|
734
|
+
pod_identity = instance_config.get_iam_role()
|
|
735
|
+
if assume_role_arn:
|
|
736
|
+
pod_identity = assume_role_arn
|
|
737
|
+
if assume_pod_identity and not pod_identity:
|
|
738
|
+
print(
|
|
739
|
+
f"Error: --assume-pod-identity passed but no pod identity was found for this instance ({instance_config.instance})",
|
|
740
|
+
file=sys.stderr,
|
|
741
|
+
)
|
|
742
|
+
sys.exit(1)
|
|
743
|
+
|
|
744
|
+
if pod_identity and (assume_pod_identity or assume_role_arn):
|
|
745
|
+
print(
|
|
746
|
+
"Calling aws-okta to assume role {} using account {}".format(
|
|
747
|
+
pod_identity, aws_account
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
elif use_okta_role:
|
|
751
|
+
print(f"Calling aws-okta using account {aws_account}")
|
|
752
|
+
elif "AWS_ROLE_ARN" in os.environ and "AWS_WEB_IDENTITY_TOKEN_FILE" in os.environ:
|
|
753
|
+
# Get a session using the current pod identity
|
|
754
|
+
print(
|
|
755
|
+
f"Found Pod Identity token in env. Assuming into role {os.environ['AWS_ROLE_ARN']}."
|
|
756
|
+
)
|
|
757
|
+
boto_session = boto3.Session()
|
|
758
|
+
credentials = boto_session.get_credentials()
|
|
759
|
+
assumed_creds_dict: AWSSessionCreds = {
|
|
760
|
+
"AWS_ACCESS_KEY_ID": credentials.access_key,
|
|
761
|
+
"AWS_SECRET_ACCESS_KEY": credentials.secret_key,
|
|
762
|
+
"AWS_SESSION_TOKEN": credentials.token,
|
|
763
|
+
"AWS_SECURITY_TOKEN": credentials.token,
|
|
764
|
+
}
|
|
765
|
+
return assumed_creds_dict
|
|
766
|
+
else:
|
|
767
|
+
# use_okta_role, assume_pod_identity, and assume_role are all empty, and there's no
|
|
768
|
+
# pod identity (web identity token) in the env. This shouldn't happen
|
|
769
|
+
print(
|
|
770
|
+
"Error: assume_aws_role called without required arguments and no pod identity env",
|
|
771
|
+
file=sys.stderr,
|
|
772
|
+
)
|
|
773
|
+
sys.exit(1)
|
|
774
|
+
# local-run will sometimes run as root - make sure that we get the actual
|
|
775
|
+
# users AWS credentials instead of looking for non-existent root AWS
|
|
776
|
+
# credentials
|
|
777
|
+
if os.getuid() == 0:
|
|
778
|
+
aws_okta_cmd = [
|
|
779
|
+
"sudo",
|
|
780
|
+
"-u",
|
|
781
|
+
get_username(),
|
|
782
|
+
f"HOME=/nail/home/{get_username()}",
|
|
783
|
+
"aws-okta",
|
|
784
|
+
"-a",
|
|
785
|
+
aws_account,
|
|
786
|
+
"-o",
|
|
787
|
+
"json",
|
|
788
|
+
]
|
|
789
|
+
else:
|
|
790
|
+
aws_okta_cmd = ["aws-okta", "-a", aws_account, "-o", "json"]
|
|
791
|
+
cmd = subprocess.run(aws_okta_cmd, stdout=subprocess.PIPE)
|
|
792
|
+
if cmd.returncode != 0:
|
|
793
|
+
print(
|
|
794
|
+
"Error calling aws-okta. Remove --assume-pod-identity to run without pod identity role",
|
|
795
|
+
file=sys.stderr,
|
|
796
|
+
)
|
|
797
|
+
sys.exit(1)
|
|
798
|
+
cmd_output = json.loads(cmd.stdout.decode("utf-8"))
|
|
799
|
+
|
|
800
|
+
if not use_okta_role:
|
|
801
|
+
boto_session = boto3.Session(
|
|
802
|
+
aws_access_key_id=cmd_output["AccessKeyId"],
|
|
803
|
+
aws_secret_access_key=cmd_output["SecretAccessKey"],
|
|
804
|
+
aws_session_token=cmd_output["SessionToken"],
|
|
805
|
+
)
|
|
806
|
+
sts_client = boto_session.client("sts")
|
|
807
|
+
assumed_role = sts_client.assume_role(
|
|
808
|
+
RoleArn=pod_identity, RoleSessionName=f"{get_username()}-local-run"
|
|
809
|
+
)
|
|
810
|
+
# The contents of "Credentials" key from assume_role is the same as from aws-okta
|
|
811
|
+
cmd_output = assumed_role["Credentials"]
|
|
812
|
+
|
|
813
|
+
creds_dict: AWSSessionCreds = {
|
|
814
|
+
"AWS_ACCESS_KEY_ID": cmd_output["AccessKeyId"],
|
|
815
|
+
"AWS_SECRET_ACCESS_KEY": cmd_output["SecretAccessKey"],
|
|
816
|
+
"AWS_SESSION_TOKEN": cmd_output["SessionToken"],
|
|
817
|
+
"AWS_SECURITY_TOKEN": cmd_output["SessionToken"],
|
|
818
|
+
}
|
|
819
|
+
return creds_dict
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def run_docker_container(
|
|
823
|
+
docker_client,
|
|
824
|
+
service,
|
|
825
|
+
instance,
|
|
826
|
+
docker_url,
|
|
827
|
+
volumes,
|
|
828
|
+
interactive,
|
|
829
|
+
command,
|
|
830
|
+
healthcheck,
|
|
831
|
+
healthcheck_only,
|
|
832
|
+
user_port,
|
|
833
|
+
instance_config,
|
|
834
|
+
secret_provider_name,
|
|
835
|
+
soa_dir=DEFAULT_SOA_DIR,
|
|
836
|
+
dry_run=False,
|
|
837
|
+
json_dict=False,
|
|
838
|
+
framework=None,
|
|
839
|
+
secret_provider_kwargs={},
|
|
840
|
+
skip_secrets=False,
|
|
841
|
+
assume_pod_identity=False,
|
|
842
|
+
assume_role_arn="",
|
|
843
|
+
use_okta_role=False,
|
|
844
|
+
assume_role_aws_account: Optional[str] = None,
|
|
845
|
+
use_service_auth_token: bool = False,
|
|
846
|
+
use_sso_service_auth_token: bool = False,
|
|
847
|
+
):
|
|
848
|
+
"""docker-py has issues running a container with a TTY attached, so for
|
|
849
|
+
consistency we execute 'docker run' directly in both interactive and
|
|
850
|
+
non-interactive modes.
|
|
851
|
+
|
|
852
|
+
In non-interactive mode when the run is complete, stop the container and
|
|
853
|
+
remove it (with docker-py).
|
|
854
|
+
"""
|
|
855
|
+
if user_port:
|
|
856
|
+
if check_if_port_free(user_port):
|
|
857
|
+
chosen_port = user_port
|
|
858
|
+
else:
|
|
859
|
+
print(
|
|
860
|
+
PaastaColors.red(
|
|
861
|
+
"The chosen port is already in use!\n"
|
|
862
|
+
"Try specifying another one, or omit (--port|-o) and paasta will find a free one for you"
|
|
863
|
+
),
|
|
864
|
+
file=sys.stderr,
|
|
865
|
+
)
|
|
866
|
+
sys.exit(1)
|
|
867
|
+
else:
|
|
868
|
+
chosen_port = pick_random_port(service)
|
|
869
|
+
environment = instance_config.get_env()
|
|
870
|
+
secret_volumes = {} # type: ignore
|
|
871
|
+
if not skip_secrets:
|
|
872
|
+
# if secrets_for_owner_team enabled in yelpsoa for service
|
|
873
|
+
if is_secrets_for_teams_enabled(service, soa_dir):
|
|
874
|
+
try:
|
|
875
|
+
kube_client = KubeClient(
|
|
876
|
+
config_file=KUBE_CONFIG_USER_PATH, context=instance_config.cluster
|
|
877
|
+
)
|
|
878
|
+
secret_environment = get_kubernetes_secret_env_variables(
|
|
879
|
+
kube_client, environment, service, instance_config.get_namespace()
|
|
880
|
+
)
|
|
881
|
+
secret_volumes = get_kubernetes_secret_volumes(
|
|
882
|
+
kube_client,
|
|
883
|
+
instance_config.get_secret_volumes(),
|
|
884
|
+
service,
|
|
885
|
+
instance_config.get_namespace(),
|
|
886
|
+
)
|
|
887
|
+
except Exception as e:
|
|
888
|
+
print(
|
|
889
|
+
f"Failed to retrieve kubernetes secrets with {e.__class__.__name__}: {e}"
|
|
890
|
+
)
|
|
891
|
+
print(
|
|
892
|
+
"If you don't need the secrets for local-run, you can add --skip-secrets"
|
|
893
|
+
)
|
|
894
|
+
sys.exit(1)
|
|
895
|
+
else:
|
|
896
|
+
try:
|
|
897
|
+
secret_environment = decrypt_secret_environment_variables(
|
|
898
|
+
secret_provider_name=secret_provider_name,
|
|
899
|
+
environment=environment,
|
|
900
|
+
soa_dir=soa_dir,
|
|
901
|
+
service_name=instance_config.get_service(),
|
|
902
|
+
cluster_name=instance_config.cluster,
|
|
903
|
+
secret_provider_kwargs=secret_provider_kwargs,
|
|
904
|
+
)
|
|
905
|
+
secret_volumes = decrypt_secret_volumes(
|
|
906
|
+
secret_provider_name=secret_provider_name,
|
|
907
|
+
secret_volumes_config=instance_config.get_secret_volumes(),
|
|
908
|
+
soa_dir=soa_dir,
|
|
909
|
+
service_name=instance_config.get_service(),
|
|
910
|
+
cluster_name=instance_config.cluster,
|
|
911
|
+
secret_provider_kwargs=secret_provider_kwargs,
|
|
912
|
+
)
|
|
913
|
+
except Exception as e:
|
|
914
|
+
print(f"Failed to decrypt secrets with {e.__class__.__name__}: {e}")
|
|
915
|
+
print(
|
|
916
|
+
"If you don't need the secrets for local-run, you can add --skip-secrets"
|
|
917
|
+
)
|
|
918
|
+
sys.exit(1)
|
|
919
|
+
environment.update(secret_environment)
|
|
920
|
+
if (
|
|
921
|
+
assume_role_arn
|
|
922
|
+
or assume_pod_identity
|
|
923
|
+
or use_okta_role
|
|
924
|
+
or "AWS_WEB_IDENTITY_TOKEN_FILE" in os.environ
|
|
925
|
+
):
|
|
926
|
+
aws_creds = assume_aws_role(
|
|
927
|
+
instance_config,
|
|
928
|
+
service,
|
|
929
|
+
assume_role_arn,
|
|
930
|
+
assume_pod_identity,
|
|
931
|
+
use_okta_role,
|
|
932
|
+
assume_role_aws_account,
|
|
933
|
+
)
|
|
934
|
+
environment.update(aws_creds)
|
|
935
|
+
|
|
936
|
+
if use_service_auth_token:
|
|
937
|
+
environment["YELP_SVC_AUTHZ_TOKEN"] = get_service_auth_token()
|
|
938
|
+
elif use_sso_service_auth_token:
|
|
939
|
+
environment["YELP_SVC_AUTHZ_TOKEN"] = get_sso_auth_token()
|
|
940
|
+
|
|
941
|
+
local_run_environment = get_local_run_environment_vars(
|
|
942
|
+
instance_config=instance_config, port0=chosen_port, framework=framework
|
|
943
|
+
)
|
|
944
|
+
environment.update(local_run_environment)
|
|
945
|
+
net = instance_config.get_net()
|
|
946
|
+
memory = instance_config.get_mem()
|
|
947
|
+
container_name = get_container_name()
|
|
948
|
+
docker_params = instance_config.format_docker_parameters()
|
|
949
|
+
|
|
950
|
+
healthcheck_mode, healthcheck_data = get_healthcheck_for_instance(
|
|
951
|
+
service, instance, instance_config, chosen_port, soa_dir=soa_dir
|
|
952
|
+
)
|
|
953
|
+
if healthcheck_mode is None:
|
|
954
|
+
container_port = None
|
|
955
|
+
interactive = True
|
|
956
|
+
elif not user_port and not healthcheck and not healthcheck_only:
|
|
957
|
+
container_port = None
|
|
958
|
+
else:
|
|
959
|
+
try:
|
|
960
|
+
container_port = instance_config.get_container_port()
|
|
961
|
+
except AttributeError:
|
|
962
|
+
container_port = None
|
|
963
|
+
|
|
964
|
+
simulate_healthcheck = (
|
|
965
|
+
healthcheck_only or healthcheck
|
|
966
|
+
) and healthcheck_mode is not None
|
|
967
|
+
|
|
968
|
+
for container_mount_path, secret_content in secret_volumes.items():
|
|
969
|
+
temp_secret_folder = tempfile.mktemp(dir=os.environ.get("TMPDIR", "/nail/tmp"))
|
|
970
|
+
os.makedirs(temp_secret_folder, exist_ok=True)
|
|
971
|
+
temp_secret_filename = os.path.join(temp_secret_folder, str(uuid.uuid4()))
|
|
972
|
+
# write the secret contents
|
|
973
|
+
# Permissions will automatically be set to readable by "users" group
|
|
974
|
+
# TODO: Make this readable only by "nobody" user? What about other non-standard users that people sometimes use inside the container?
|
|
975
|
+
# -rw-r--r-- 1 dpopes users 3.2K Nov 28 19:16 854bdbad-30b8-4681-ae4e-854cb28075c5
|
|
976
|
+
try:
|
|
977
|
+
# First try to write the file as a string
|
|
978
|
+
# This is for text like config files
|
|
979
|
+
with open(temp_secret_filename, "w") as f:
|
|
980
|
+
f.write(secret_content)
|
|
981
|
+
except TypeError:
|
|
982
|
+
# If that fails, try to write it as bytes
|
|
983
|
+
# This is for binary files like TLS keys
|
|
984
|
+
with open(temp_secret_filename, "wb") as fb:
|
|
985
|
+
fb.write(secret_content)
|
|
986
|
+
|
|
987
|
+
# Append this to the list of volumes passed to docker run
|
|
988
|
+
volumes.append(f"{temp_secret_filename}:{container_mount_path}:ro")
|
|
989
|
+
|
|
990
|
+
docker_run_args = dict(
|
|
991
|
+
memory=memory,
|
|
992
|
+
chosen_port=chosen_port,
|
|
993
|
+
container_port=container_port,
|
|
994
|
+
container_name=container_name,
|
|
995
|
+
volumes=volumes,
|
|
996
|
+
env=environment,
|
|
997
|
+
interactive=interactive,
|
|
998
|
+
detach=simulate_healthcheck,
|
|
999
|
+
docker_hash=docker_url,
|
|
1000
|
+
command=command,
|
|
1001
|
+
net=net,
|
|
1002
|
+
docker_params=docker_params,
|
|
1003
|
+
)
|
|
1004
|
+
docker_run_cmd = get_docker_run_cmd(**docker_run_args)
|
|
1005
|
+
joined_docker_run_cmd = " ".join(docker_run_cmd)
|
|
1006
|
+
|
|
1007
|
+
if dry_run:
|
|
1008
|
+
if json_dict:
|
|
1009
|
+
print(json.dumps(docker_run_args))
|
|
1010
|
+
else:
|
|
1011
|
+
print(json.dumps(docker_run_cmd))
|
|
1012
|
+
return 0
|
|
1013
|
+
else:
|
|
1014
|
+
print("Running docker command:\n%s" % PaastaColors.grey(joined_docker_run_cmd))
|
|
1015
|
+
|
|
1016
|
+
merged_env = {**os.environ, **environment}
|
|
1017
|
+
|
|
1018
|
+
if interactive or not simulate_healthcheck:
|
|
1019
|
+
# NOTE: This immediately replaces us with the docker run cmd. Docker
|
|
1020
|
+
# run knows how to clean up the running container in this situation.
|
|
1021
|
+
wrapper_path = shutil.which("paasta_docker_wrapper")
|
|
1022
|
+
# To properly simulate mesos, we pop the PATH, which is not available to
|
|
1023
|
+
# The executor
|
|
1024
|
+
merged_env.pop("PATH")
|
|
1025
|
+
execlpe(wrapper_path, *docker_run_cmd, merged_env)
|
|
1026
|
+
# For testing, when execlpe is patched out and doesn't replace us, we
|
|
1027
|
+
# still want to bail out.
|
|
1028
|
+
return 0
|
|
1029
|
+
|
|
1030
|
+
container_started = False
|
|
1031
|
+
container_id = None
|
|
1032
|
+
try:
|
|
1033
|
+
(returncode, output) = _run(docker_run_cmd, env=merged_env)
|
|
1034
|
+
if returncode != 0:
|
|
1035
|
+
print(
|
|
1036
|
+
"Failure trying to start your container!"
|
|
1037
|
+
"Returncode: %d"
|
|
1038
|
+
"Output:"
|
|
1039
|
+
"%s"
|
|
1040
|
+
""
|
|
1041
|
+
"Fix that problem and try again."
|
|
1042
|
+
"http://y/paasta-troubleshooting" % (returncode, output),
|
|
1043
|
+
sep="\n",
|
|
1044
|
+
)
|
|
1045
|
+
# Container failed to start so no need to cleanup; just bail.
|
|
1046
|
+
sys.exit(1)
|
|
1047
|
+
container_started = True
|
|
1048
|
+
container_id = get_container_id(docker_client, container_name)
|
|
1049
|
+
print("Found our container running with CID %s" % container_id)
|
|
1050
|
+
|
|
1051
|
+
if simulate_healthcheck:
|
|
1052
|
+
healthcheck_result = simulate_healthcheck_on_service(
|
|
1053
|
+
instance_config=instance_config,
|
|
1054
|
+
docker_client=docker_client,
|
|
1055
|
+
container_id=container_id,
|
|
1056
|
+
healthcheck_mode=healthcheck_mode,
|
|
1057
|
+
healthcheck_data=healthcheck_data,
|
|
1058
|
+
healthcheck_enabled=healthcheck,
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
def _output_exit_code():
|
|
1062
|
+
returncode = docker_client.inspect_container(container_id)["State"][
|
|
1063
|
+
"ExitCode"
|
|
1064
|
+
]
|
|
1065
|
+
print(f"Container exited: {returncode})")
|
|
1066
|
+
|
|
1067
|
+
if healthcheck_only:
|
|
1068
|
+
if container_started:
|
|
1069
|
+
_output_exit_code()
|
|
1070
|
+
_cleanup_container(docker_client, container_id)
|
|
1071
|
+
if healthcheck_mode is None:
|
|
1072
|
+
print(
|
|
1073
|
+
"--healthcheck-only, but no healthcheck is defined for this instance!"
|
|
1074
|
+
)
|
|
1075
|
+
sys.exit(1)
|
|
1076
|
+
elif healthcheck_result is True:
|
|
1077
|
+
sys.exit(0)
|
|
1078
|
+
else:
|
|
1079
|
+
sys.exit(1)
|
|
1080
|
+
|
|
1081
|
+
running = docker_client.inspect_container(container_id)["State"]["Running"]
|
|
1082
|
+
if running:
|
|
1083
|
+
print("Your service is now running! Tailing stdout and stderr:")
|
|
1084
|
+
for line in docker_client.logs(
|
|
1085
|
+
container_id,
|
|
1086
|
+
stderr=True,
|
|
1087
|
+
stream=True,
|
|
1088
|
+
):
|
|
1089
|
+
# writing to sys.stdout.buffer lets us write the raw bytes we
|
|
1090
|
+
# get from the docker client without having to convert them to
|
|
1091
|
+
# a utf-8 string
|
|
1092
|
+
sys.stdout.buffer.write(line)
|
|
1093
|
+
sys.stdout.flush()
|
|
1094
|
+
else:
|
|
1095
|
+
_output_exit_code()
|
|
1096
|
+
returncode = 3
|
|
1097
|
+
|
|
1098
|
+
except KeyboardInterrupt:
|
|
1099
|
+
returncode = 3
|
|
1100
|
+
|
|
1101
|
+
# Cleanup if the container exits on its own or interrupted.
|
|
1102
|
+
if container_started:
|
|
1103
|
+
returncode = docker_client.inspect_container(container_id)["State"]["ExitCode"]
|
|
1104
|
+
_cleanup_container(docker_client, container_id)
|
|
1105
|
+
return returncode
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def format_command_for_type(command, instance_type, date):
|
|
1109
|
+
"""
|
|
1110
|
+
Given an instance_type, return a function that appropriately formats
|
|
1111
|
+
the command to be run.
|
|
1112
|
+
"""
|
|
1113
|
+
if instance_type == "tron":
|
|
1114
|
+
interpolated_command = parse_time_variables(command, date)
|
|
1115
|
+
return interpolated_command
|
|
1116
|
+
else:
|
|
1117
|
+
return command
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def configure_and_run_docker_container(
|
|
1121
|
+
docker_client,
|
|
1122
|
+
docker_url,
|
|
1123
|
+
docker_sha,
|
|
1124
|
+
service,
|
|
1125
|
+
instance,
|
|
1126
|
+
cluster,
|
|
1127
|
+
system_paasta_config,
|
|
1128
|
+
args,
|
|
1129
|
+
assume_role_aws_account,
|
|
1130
|
+
pull_image=False,
|
|
1131
|
+
dry_run=False,
|
|
1132
|
+
):
|
|
1133
|
+
"""
|
|
1134
|
+
Run Docker container by image hash with args set in command line.
|
|
1135
|
+
Function prints the output of run command in stdout.
|
|
1136
|
+
"""
|
|
1137
|
+
|
|
1138
|
+
if instance is None and args.healthcheck_only:
|
|
1139
|
+
print("With --healthcheck-only, --instance MUST be provided!", file=sys.stderr)
|
|
1140
|
+
return 1
|
|
1141
|
+
if instance is None and not sys.stdin.isatty():
|
|
1142
|
+
print(
|
|
1143
|
+
"--instance and --cluster must be specified when using paasta local-run without a tty!",
|
|
1144
|
+
file=sys.stderr,
|
|
1145
|
+
)
|
|
1146
|
+
return 1
|
|
1147
|
+
|
|
1148
|
+
soa_dir = args.yelpsoa_config_root
|
|
1149
|
+
volumes = args.volumes
|
|
1150
|
+
load_deployments = (docker_url is None or pull_image) and not docker_sha
|
|
1151
|
+
interactive = args.interactive
|
|
1152
|
+
|
|
1153
|
+
try:
|
|
1154
|
+
if instance is None:
|
|
1155
|
+
instance_type = "adhoc"
|
|
1156
|
+
instance = "interactive"
|
|
1157
|
+
instance_config = get_default_interactive_config(
|
|
1158
|
+
service=service,
|
|
1159
|
+
cluster=cluster,
|
|
1160
|
+
soa_dir=soa_dir,
|
|
1161
|
+
load_deployments=load_deployments,
|
|
1162
|
+
)
|
|
1163
|
+
interactive = True
|
|
1164
|
+
else:
|
|
1165
|
+
instance_type = validate_service_instance(
|
|
1166
|
+
service, instance, cluster, soa_dir
|
|
1167
|
+
)
|
|
1168
|
+
instance_config = get_instance_config(
|
|
1169
|
+
service=service,
|
|
1170
|
+
instance=instance,
|
|
1171
|
+
cluster=cluster,
|
|
1172
|
+
load_deployments=load_deployments,
|
|
1173
|
+
soa_dir=soa_dir,
|
|
1174
|
+
)
|
|
1175
|
+
except NoConfigurationForServiceError as e:
|
|
1176
|
+
print(str(e), file=sys.stderr)
|
|
1177
|
+
return 1
|
|
1178
|
+
except NoDeploymentsAvailable:
|
|
1179
|
+
print(
|
|
1180
|
+
PaastaColors.red(
|
|
1181
|
+
"Error: No deployments.json found in %(soa_dir)s/%(service)s. "
|
|
1182
|
+
"You can generate this by running: "
|
|
1183
|
+
"generate_deployments_for_service -d %(soa_dir)s -s %(service)s"
|
|
1184
|
+
% {"soa_dir": soa_dir, "service": service}
|
|
1185
|
+
),
|
|
1186
|
+
sep="\n",
|
|
1187
|
+
file=sys.stderr,
|
|
1188
|
+
)
|
|
1189
|
+
return 1
|
|
1190
|
+
|
|
1191
|
+
if docker_sha is not None:
|
|
1192
|
+
instance_config.branch_dict = {
|
|
1193
|
+
"git_sha": docker_sha,
|
|
1194
|
+
"docker_image": build_docker_image_name(service=service, sha=docker_sha),
|
|
1195
|
+
"desired_state": "start",
|
|
1196
|
+
"force_bounce": None,
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if docker_url is None:
|
|
1200
|
+
try:
|
|
1201
|
+
docker_url = instance_config.get_docker_url()
|
|
1202
|
+
except NoDockerImageError:
|
|
1203
|
+
if instance_config.get_deploy_group() is None:
|
|
1204
|
+
print(
|
|
1205
|
+
PaastaColors.red(
|
|
1206
|
+
f"Error: {service}.{instance} has no 'deploy_group' set. Please set one so "
|
|
1207
|
+
"the proper image can be used to run for this service."
|
|
1208
|
+
),
|
|
1209
|
+
sep="",
|
|
1210
|
+
file=sys.stderr,
|
|
1211
|
+
)
|
|
1212
|
+
else:
|
|
1213
|
+
print(
|
|
1214
|
+
PaastaColors.red(
|
|
1215
|
+
"Error: No sha has been marked for deployment for the %s deploy group.\n"
|
|
1216
|
+
"Please ensure this service has either run through a jenkins pipeline "
|
|
1217
|
+
"or paasta mark-for-deployment has been run for %s\n"
|
|
1218
|
+
% (instance_config.get_deploy_group(), service)
|
|
1219
|
+
),
|
|
1220
|
+
sep="",
|
|
1221
|
+
file=sys.stderr,
|
|
1222
|
+
)
|
|
1223
|
+
return 1
|
|
1224
|
+
|
|
1225
|
+
if pull_image:
|
|
1226
|
+
docker_pull_image(docker_url)
|
|
1227
|
+
|
|
1228
|
+
for volume in instance_config.get_volumes(
|
|
1229
|
+
system_paasta_config.get_volumes(),
|
|
1230
|
+
):
|
|
1231
|
+
if os.path.exists(volume["hostPath"]):
|
|
1232
|
+
volumes.append(
|
|
1233
|
+
"{}:{}:{}".format(
|
|
1234
|
+
volume["hostPath"], volume["containerPath"], volume["mode"].lower()
|
|
1235
|
+
)
|
|
1236
|
+
)
|
|
1237
|
+
else:
|
|
1238
|
+
print(
|
|
1239
|
+
PaastaColors.yellow(
|
|
1240
|
+
"Warning: Path %s does not exist on this host. Skipping this binding."
|
|
1241
|
+
% volume["hostPath"]
|
|
1242
|
+
),
|
|
1243
|
+
file=sys.stderr,
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
if interactive is True and args.cmd is None:
|
|
1247
|
+
command = "bash"
|
|
1248
|
+
elif args.cmd:
|
|
1249
|
+
command = args.cmd
|
|
1250
|
+
else:
|
|
1251
|
+
command_from_config = instance_config.get_cmd()
|
|
1252
|
+
if command_from_config:
|
|
1253
|
+
command = format_command_for_type(
|
|
1254
|
+
command=command_from_config, instance_type=instance_type, date=args.date
|
|
1255
|
+
)
|
|
1256
|
+
else:
|
|
1257
|
+
command = instance_config.get_args()
|
|
1258
|
+
|
|
1259
|
+
secret_provider_kwargs = {
|
|
1260
|
+
"vault_cluster_config": system_paasta_config.get_vault_cluster_config(),
|
|
1261
|
+
"vault_auth_method": args.vault_auth_method,
|
|
1262
|
+
"vault_token_file": args.vault_token_file,
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return run_docker_container(
|
|
1266
|
+
docker_client=docker_client,
|
|
1267
|
+
service=service,
|
|
1268
|
+
instance=instance,
|
|
1269
|
+
docker_url=docker_url,
|
|
1270
|
+
volumes=volumes,
|
|
1271
|
+
interactive=interactive,
|
|
1272
|
+
command=command,
|
|
1273
|
+
healthcheck=args.healthcheck,
|
|
1274
|
+
healthcheck_only=args.healthcheck_only,
|
|
1275
|
+
user_port=args.user_port,
|
|
1276
|
+
instance_config=instance_config,
|
|
1277
|
+
soa_dir=args.yelpsoa_config_root,
|
|
1278
|
+
dry_run=dry_run,
|
|
1279
|
+
json_dict=args.dry_run_json_dict,
|
|
1280
|
+
framework=instance_type,
|
|
1281
|
+
secret_provider_name=system_paasta_config.get_secret_provider_name(),
|
|
1282
|
+
secret_provider_kwargs=secret_provider_kwargs,
|
|
1283
|
+
skip_secrets=args.skip_secrets,
|
|
1284
|
+
assume_pod_identity=args.assume_pod_identity,
|
|
1285
|
+
assume_role_arn=args.assume_role_arn,
|
|
1286
|
+
assume_role_aws_account=assume_role_aws_account,
|
|
1287
|
+
use_okta_role=args.use_okta_role,
|
|
1288
|
+
use_service_auth_token=args.use_service_auth_token,
|
|
1289
|
+
use_sso_service_auth_token=args.use_sso_service_auth_token,
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def docker_config_available():
|
|
1294
|
+
home = os.path.expanduser("~")
|
|
1295
|
+
oldconfig = os.path.join(home, ".dockercfg")
|
|
1296
|
+
newconfig = os.path.join(home, ".docker", "config.json")
|
|
1297
|
+
return (os.path.isfile(oldconfig) and os.access(oldconfig, os.R_OK)) or (
|
|
1298
|
+
os.path.isfile(newconfig) and os.access(newconfig, os.R_OK)
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def paasta_local_run(args):
|
|
1303
|
+
if args.action == "pull" and os.geteuid() != 0 and not docker_config_available():
|
|
1304
|
+
print("Re-executing paasta local-run --pull with sudo..")
|
|
1305
|
+
os.execvp("sudo", ["sudo", "-H"] + sys.argv)
|
|
1306
|
+
if args.action == "build" and not makefile_responds_to("cook-image"):
|
|
1307
|
+
print(
|
|
1308
|
+
"A local Makefile with a 'cook-image' target is required for --build",
|
|
1309
|
+
file=sys.stderr,
|
|
1310
|
+
)
|
|
1311
|
+
print(
|
|
1312
|
+
"If you meant to pull the docker image from the registry, explicitly pass --pull",
|
|
1313
|
+
file=sys.stderr,
|
|
1314
|
+
)
|
|
1315
|
+
return 1
|
|
1316
|
+
|
|
1317
|
+
try:
|
|
1318
|
+
system_paasta_config = load_system_paasta_config()
|
|
1319
|
+
except PaastaNotConfiguredError:
|
|
1320
|
+
print(
|
|
1321
|
+
PaastaColors.yellow(
|
|
1322
|
+
"Warning: Couldn't load config files from '/etc/paasta'. This indicates"
|
|
1323
|
+
"PaaSTA is not configured locally on this host, and local-run may not behave"
|
|
1324
|
+
"the same way it would behave on a server configured for PaaSTA."
|
|
1325
|
+
),
|
|
1326
|
+
sep="\n",
|
|
1327
|
+
)
|
|
1328
|
+
system_paasta_config = SystemPaastaConfig({"volumes": []}, "/etc/paasta")
|
|
1329
|
+
|
|
1330
|
+
local_run_config = system_paasta_config.get_local_run_config()
|
|
1331
|
+
|
|
1332
|
+
service = figure_out_service_name(args, soa_dir=args.yelpsoa_config_root)
|
|
1333
|
+
|
|
1334
|
+
if args.cluster:
|
|
1335
|
+
cluster = args.cluster
|
|
1336
|
+
else:
|
|
1337
|
+
try:
|
|
1338
|
+
cluster = local_run_config["default_cluster"]
|
|
1339
|
+
except KeyError:
|
|
1340
|
+
print(
|
|
1341
|
+
PaastaColors.red(
|
|
1342
|
+
"PaaSTA on this machine has not been configured with a default cluster."
|
|
1343
|
+
"Please pass one to local-run using '-c'."
|
|
1344
|
+
),
|
|
1345
|
+
sep="\n",
|
|
1346
|
+
file=sys.stderr,
|
|
1347
|
+
)
|
|
1348
|
+
return 1
|
|
1349
|
+
assume_role_aws_account = args.assume_role_aws_account or (
|
|
1350
|
+
system_paasta_config.get_kube_clusters()
|
|
1351
|
+
.get(cluster, {})
|
|
1352
|
+
.get("aws_account", resolve_aws_account_from_runtimeenv())
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
instance = args.instance
|
|
1356
|
+
docker_client = get_docker_client()
|
|
1357
|
+
|
|
1358
|
+
docker_sha = None
|
|
1359
|
+
docker_url = None
|
|
1360
|
+
|
|
1361
|
+
if args.action == "build":
|
|
1362
|
+
default_tag = "paasta-local-run-{}-{}".format(service, get_username())
|
|
1363
|
+
docker_url = os.environ.get("DOCKER_TAG", default_tag)
|
|
1364
|
+
os.environ["DOCKER_TAG"] = docker_url
|
|
1365
|
+
pull_image = False
|
|
1366
|
+
cook_return = paasta_cook_image(
|
|
1367
|
+
args=None, service=service, soa_dir=args.yelpsoa_config_root
|
|
1368
|
+
)
|
|
1369
|
+
if cook_return != 0:
|
|
1370
|
+
return cook_return
|
|
1371
|
+
elif args.action == "dry_run":
|
|
1372
|
+
pull_image = False
|
|
1373
|
+
docker_url = None
|
|
1374
|
+
docker_sha = args.sha
|
|
1375
|
+
else:
|
|
1376
|
+
pull_image = True
|
|
1377
|
+
docker_url = None
|
|
1378
|
+
docker_sha = args.sha
|
|
1379
|
+
|
|
1380
|
+
try:
|
|
1381
|
+
return configure_and_run_docker_container(
|
|
1382
|
+
docker_client=docker_client,
|
|
1383
|
+
docker_url=docker_url,
|
|
1384
|
+
docker_sha=docker_sha,
|
|
1385
|
+
service=service,
|
|
1386
|
+
instance=instance,
|
|
1387
|
+
cluster=cluster,
|
|
1388
|
+
args=args,
|
|
1389
|
+
pull_image=pull_image,
|
|
1390
|
+
system_paasta_config=system_paasta_config,
|
|
1391
|
+
dry_run=args.action == "dry_run",
|
|
1392
|
+
assume_role_aws_account=assume_role_aws_account,
|
|
1393
|
+
)
|
|
1394
|
+
except errors.APIError as e:
|
|
1395
|
+
print("Can't run Docker container. Error: %s" % str(e), file=sys.stderr)
|
|
1396
|
+
return 1
|