capo-codedeploy 0.1.0__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.
- capo_codedeploy/__init__.py +70 -0
- capo_codedeploy/_async.py +25 -0
- capo_codedeploy/_auth/_identity.py +16 -0
- capo_codedeploy/_auth/_providers.py +886 -0
- capo_codedeploy/_auth/_signers.py +88 -0
- capo_codedeploy/_auth/_sigv4.py +434 -0
- capo_codedeploy/_auth/_zapros_handler.py +80 -0
- capo_codedeploy/_iter.py +113 -0
- capo_codedeploy/_operations/code_deploy_20141006/add_tags_to_on_premises_instances.py +153 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_application_revisions.py +173 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_applications.py +163 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_deployment_groups.py +178 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_deployment_instances.py +178 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_deployment_targets.py +188 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_deployments.py +158 -0
- capo_codedeploy/_operations/code_deploy_20141006/batch_get_on_premises_instances.py +158 -0
- capo_codedeploy/_operations/code_deploy_20141006/continue_deployment.py +155 -0
- capo_codedeploy/_operations/code_deploy_20141006/create_application.py +175 -0
- capo_codedeploy/_operations/code_deploy_20141006/create_deployment.py +278 -0
- capo_codedeploy/_operations/code_deploy_20141006/create_deployment_config.py +185 -0
- capo_codedeploy/_operations/code_deploy_20141006/create_deployment_group.py +320 -0
- capo_codedeploy/_operations/code_deploy_20141006/delete_application.py +129 -0
- capo_codedeploy/_operations/code_deploy_20141006/delete_deployment_config.py +136 -0
- capo_codedeploy/_operations/code_deploy_20141006/delete_deployment_group.py +167 -0
- capo_codedeploy/_operations/code_deploy_20141006/delete_git_hub_account_token.py +166 -0
- capo_codedeploy/_operations/code_deploy_20141006/delete_resources_by_external_id.py +137 -0
- capo_codedeploy/_operations/code_deploy_20141006/deregister_on_premises_instance.py +126 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_application.py +157 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_application_revision.py +173 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_deployment.py +157 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_deployment_config.py +162 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_deployment_group.py +179 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_deployment_instance.py +177 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_deployment_target.py +182 -0
- capo_codedeploy/_operations/code_deploy_20141006/get_on_premises_instance.py +157 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_application_revisions.py +195 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_applications.py +149 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_deployment_configs.py +147 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_deployment_groups.py +162 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_deployment_instances.py +198 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_deployment_targets.py +188 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_deployments.py +199 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_git_hub_account_token_names.py +157 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_on_premises_instances.py +159 -0
- capo_codedeploy/_operations/code_deploy_20141006/list_tags_for_resource.py +157 -0
- capo_codedeploy/_operations/code_deploy_20141006/put_lifecycle_event_hook_execution_status.py +177 -0
- capo_codedeploy/_operations/code_deploy_20141006/register_application_revision.py +147 -0
- capo_codedeploy/_operations/code_deploy_20141006/register_on_premises_instance.py +166 -0
- capo_codedeploy/_operations/code_deploy_20141006/remove_tags_from_on_premises_instances.py +153 -0
- capo_codedeploy/_operations/code_deploy_20141006/skip_wait_time_for_instance_termination.py +146 -0
- capo_codedeploy/_operations/code_deploy_20141006/stop_deployment.py +172 -0
- capo_codedeploy/_operations/code_deploy_20141006/tag_resource.py +174 -0
- capo_codedeploy/_operations/code_deploy_20141006/untag_resource.py +174 -0
- capo_codedeploy/_operations/code_deploy_20141006/update_application.py +134 -0
- capo_codedeploy/_operations/code_deploy_20141006/update_deployment_group.py +310 -0
- capo_codedeploy/_pagination.py +21 -0
- capo_codedeploy/_protocol/__init__.py +1 -0
- capo_codedeploy/_protocol/errors.py +93 -0
- capo_codedeploy/_protocol/eventstream.py +238 -0
- capo_codedeploy/_protocol/serialize.py +47 -0
- capo_codedeploy/_protocol/xml.py +33 -0
- capo_codedeploy/_rule_engine/__init__.py +0 -0
- capo_codedeploy/_rule_engine/_aws_partition.py +160 -0
- capo_codedeploy/_rule_engine/_endpoint_rule_set.py +138 -0
- capo_codedeploy/_rule_engine/_endpoint_runtime.py +389 -0
- capo_codedeploy/_services/_aws_config.py +160 -0
- capo_codedeploy/_services/_pipeline.py +198 -0
- capo_codedeploy/_services/async_code_deploy.py +3278 -0
- capo_codedeploy/_services/code_deploy.py +3229 -0
- capo_codedeploy/errors/__init__.py +335 -0
- capo_codedeploy/errors/_base.py +94 -0
- capo_codedeploy/errors/alarms_limit_exceeded_exception.py +52 -0
- capo_codedeploy/errors/application_already_exists_exception.py +54 -0
- capo_codedeploy/errors/application_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/application_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/application_name_required_exception.py +54 -0
- capo_codedeploy/errors/arn_not_supported_exception.py +52 -0
- capo_codedeploy/errors/batch_limit_exceeded_exception.py +52 -0
- capo_codedeploy/errors/bucket_name_filter_required_exception.py +54 -0
- capo_codedeploy/errors/deployment_already_completed_exception.py +54 -0
- capo_codedeploy/errors/deployment_config_already_exists_exception.py +54 -0
- capo_codedeploy/errors/deployment_config_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/deployment_config_in_use_exception.py +54 -0
- capo_codedeploy/errors/deployment_config_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/deployment_config_name_required_exception.py +54 -0
- capo_codedeploy/errors/deployment_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/deployment_group_already_exists_exception.py +54 -0
- capo_codedeploy/errors/deployment_group_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/deployment_group_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/deployment_group_name_required_exception.py +54 -0
- capo_codedeploy/errors/deployment_id_required_exception.py +54 -0
- capo_codedeploy/errors/deployment_is_not_in_ready_state_exception.py +54 -0
- capo_codedeploy/errors/deployment_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/deployment_not_started_exception.py +54 -0
- capo_codedeploy/errors/deployment_target_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/deployment_target_id_required_exception.py +54 -0
- capo_codedeploy/errors/deployment_target_list_size_exceeded_exception.py +56 -0
- capo_codedeploy/errors/description_too_long_exception.py +52 -0
- capo_codedeploy/errors/ecs_service_mapping_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/git_hub_account_token_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/git_hub_account_token_name_required_exception.py +54 -0
- capo_codedeploy/errors/iam_arn_required_exception.py +52 -0
- capo_codedeploy/errors/iam_session_arn_already_registered_exception.py +54 -0
- capo_codedeploy/errors/iam_user_arn_already_registered_exception.py +54 -0
- capo_codedeploy/errors/iam_user_arn_required_exception.py +52 -0
- capo_codedeploy/errors/instance_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/instance_id_required_exception.py +52 -0
- capo_codedeploy/errors/instance_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/instance_name_already_registered_exception.py +54 -0
- capo_codedeploy/errors/instance_name_required_exception.py +54 -0
- capo_codedeploy/errors/instance_not_registered_exception.py +54 -0
- capo_codedeploy/errors/invalid_alarm_config_exception.py +52 -0
- capo_codedeploy/errors/invalid_application_name_exception.py +54 -0
- capo_codedeploy/errors/invalid_arn_exception.py +52 -0
- capo_codedeploy/errors/invalid_auto_rollback_config_exception.py +54 -0
- capo_codedeploy/errors/invalid_auto_scaling_group_exception.py +54 -0
- capo_codedeploy/errors/invalid_blue_green_deployment_configuration_exception.py +60 -0
- capo_codedeploy/errors/invalid_bucket_name_filter_exception.py +54 -0
- capo_codedeploy/errors/invalid_compute_platform_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployed_state_filter_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_config_name_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_group_name_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_id_exception.py +52 -0
- capo_codedeploy/errors/invalid_deployment_instance_type_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_status_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_style_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_target_id_exception.py +54 -0
- capo_codedeploy/errors/invalid_deployment_wait_type_exception.py +54 -0
- capo_codedeploy/errors/invalid_ec2_tag_combination_exception.py +54 -0
- capo_codedeploy/errors/invalid_ec2_tag_exception.py +52 -0
- capo_codedeploy/errors/invalid_ecs_service_exception.py +52 -0
- capo_codedeploy/errors/invalid_external_id_exception.py +52 -0
- capo_codedeploy/errors/invalid_file_exists_behavior_exception.py +54 -0
- capo_codedeploy/errors/invalid_git_hub_account_token_exception.py +54 -0
- capo_codedeploy/errors/invalid_git_hub_account_token_name_exception.py +54 -0
- capo_codedeploy/errors/invalid_iam_session_arn_exception.py +54 -0
- capo_codedeploy/errors/invalid_iam_user_arn_exception.py +52 -0
- capo_codedeploy/errors/invalid_ignore_application_stop_failures_value_exception.py +60 -0
- capo_codedeploy/errors/invalid_input_exception.py +52 -0
- capo_codedeploy/errors/invalid_instance_name_exception.py +52 -0
- capo_codedeploy/errors/invalid_instance_status_exception.py +54 -0
- capo_codedeploy/errors/invalid_instance_type_exception.py +52 -0
- capo_codedeploy/errors/invalid_key_prefix_filter_exception.py +54 -0
- capo_codedeploy/errors/invalid_lifecycle_event_hook_execution_id_exception.py +60 -0
- capo_codedeploy/errors/invalid_lifecycle_event_hook_execution_status_exception.py +60 -0
- capo_codedeploy/errors/invalid_load_balancer_info_exception.py +54 -0
- capo_codedeploy/errors/invalid_minimum_healthy_host_value_exception.py +54 -0
- capo_codedeploy/errors/invalid_next_token_exception.py +52 -0
- capo_codedeploy/errors/invalid_on_premises_tag_combination_exception.py +56 -0
- capo_codedeploy/errors/invalid_operation_exception.py +52 -0
- capo_codedeploy/errors/invalid_registration_status_exception.py +54 -0
- capo_codedeploy/errors/invalid_revision_exception.py +52 -0
- capo_codedeploy/errors/invalid_role_exception.py +52 -0
- capo_codedeploy/errors/invalid_sort_by_exception.py +52 -0
- capo_codedeploy/errors/invalid_sort_order_exception.py +52 -0
- capo_codedeploy/errors/invalid_tag_exception.py +52 -0
- capo_codedeploy/errors/invalid_tag_filter_exception.py +52 -0
- capo_codedeploy/errors/invalid_tags_to_add_exception.py +52 -0
- capo_codedeploy/errors/invalid_target_filter_name_exception.py +54 -0
- capo_codedeploy/errors/invalid_target_group_pair_exception.py +54 -0
- capo_codedeploy/errors/invalid_target_instances_exception.py +54 -0
- capo_codedeploy/errors/invalid_time_range_exception.py +52 -0
- capo_codedeploy/errors/invalid_traffic_routing_configuration_exception.py +58 -0
- capo_codedeploy/errors/invalid_trigger_config_exception.py +54 -0
- capo_codedeploy/errors/invalid_update_outdated_instances_only_value_exception.py +60 -0
- capo_codedeploy/errors/invalid_zonal_deployment_configuration_exception.py +60 -0
- capo_codedeploy/errors/lifecycle_event_already_completed_exception.py +54 -0
- capo_codedeploy/errors/lifecycle_hook_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/multiple_iam_arns_provided_exception.py +54 -0
- capo_codedeploy/errors/operation_not_supported_exception.py +54 -0
- capo_codedeploy/errors/resource_arn_required_exception.py +52 -0
- capo_codedeploy/errors/resource_validation_exception.py +52 -0
- capo_codedeploy/errors/revision_does_not_exist_exception.py +54 -0
- capo_codedeploy/errors/revision_required_exception.py +52 -0
- capo_codedeploy/errors/role_required_exception.py +52 -0
- capo_codedeploy/errors/tag_limit_exceeded_exception.py +52 -0
- capo_codedeploy/errors/tag_required_exception.py +52 -0
- capo_codedeploy/errors/tag_set_list_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/throttling_exception.py +52 -0
- capo_codedeploy/errors/trigger_targets_limit_exceeded_exception.py +54 -0
- capo_codedeploy/errors/unsupported_action_for_deployment_type_exception.py +58 -0
- capo_codedeploy/py.typed +0 -0
- capo_codedeploy/types/_prelude/blob.py +12 -0
- capo_codedeploy/types/_prelude/timestamp.py +17 -0
- capo_codedeploy/types/add_tags_to_on_premises_instances_input.py +59 -0
- capo_codedeploy/types/additional_deployment_status_info.py +5 -0
- capo_codedeploy/types/alarm.py +28 -0
- capo_codedeploy/types/alarm_configuration.py +51 -0
- capo_codedeploy/types/alarm_list.py +29 -0
- capo_codedeploy/types/alarm_name.py +5 -0
- capo_codedeploy/types/app_spec_content.py +35 -0
- capo_codedeploy/types/application_id.py +5 -0
- capo_codedeploy/types/application_info.py +90 -0
- capo_codedeploy/types/application_name.py +5 -0
- capo_codedeploy/types/application_revision_sort_by.py +18 -0
- capo_codedeploy/types/applications_info_list.py +33 -0
- capo_codedeploy/types/applications_list.py +19 -0
- capo_codedeploy/types/arn.py +5 -0
- capo_codedeploy/types/auto_rollback_configuration.py +50 -0
- capo_codedeploy/types/auto_rollback_event.py +18 -0
- capo_codedeploy/types/auto_rollback_events_list.py +35 -0
- capo_codedeploy/types/auto_scaling_group.py +47 -0
- capo_codedeploy/types/auto_scaling_group_hook.py +5 -0
- capo_codedeploy/types/auto_scaling_group_list.py +35 -0
- capo_codedeploy/types/auto_scaling_group_name.py +5 -0
- capo_codedeploy/types/auto_scaling_group_name_list.py +19 -0
- capo_codedeploy/types/batch_get_application_revisions_input.py +55 -0
- capo_codedeploy/types/batch_get_application_revisions_output.py +56 -0
- capo_codedeploy/types/batch_get_applications_input.py +45 -0
- capo_codedeploy/types/batch_get_applications_output.py +42 -0
- capo_codedeploy/types/batch_get_deployment_groups_input.py +57 -0
- capo_codedeploy/types/batch_get_deployment_groups_output.py +49 -0
- capo_codedeploy/types/batch_get_deployment_instances_input.py +53 -0
- capo_codedeploy/types/batch_get_deployment_instances_output.py +49 -0
- capo_codedeploy/types/batch_get_deployment_targets_input.py +51 -0
- capo_codedeploy/types/batch_get_deployment_targets_output.py +42 -0
- capo_codedeploy/types/batch_get_deployments_input.py +43 -0
- capo_codedeploy/types/batch_get_deployments_output.py +42 -0
- capo_codedeploy/types/batch_get_on_premises_instances_input.py +45 -0
- capo_codedeploy/types/batch_get_on_premises_instances_output.py +42 -0
- capo_codedeploy/types/blue_green_deployment_configuration.py +84 -0
- capo_codedeploy/types/blue_instance_termination_option.py +46 -0
- capo_codedeploy/types/boolean.py +5 -0
- capo_codedeploy/types/bundle_type.py +20 -0
- capo_codedeploy/types/cloud_formation_resource_type.py +5 -0
- capo_codedeploy/types/cloud_formation_target.py +112 -0
- capo_codedeploy/types/commit_id.py +5 -0
- capo_codedeploy/types/compute_platform.py +18 -0
- capo_codedeploy/types/continue_deployment_input.py +49 -0
- capo_codedeploy/types/create_application_input.py +67 -0
- capo_codedeploy/types/create_application_output.py +28 -0
- capo_codedeploy/types/create_deployment_config_input.py +115 -0
- capo_codedeploy/types/create_deployment_config_output.py +30 -0
- capo_codedeploy/types/create_deployment_group_input.py +353 -0
- capo_codedeploy/types/create_deployment_group_output.py +30 -0
- capo_codedeploy/types/create_deployment_input.py +178 -0
- capo_codedeploy/types/create_deployment_output.py +28 -0
- capo_codedeploy/types/delete_application_input.py +31 -0
- capo_codedeploy/types/delete_deployment_config_input.py +35 -0
- capo_codedeploy/types/delete_deployment_group_input.py +45 -0
- capo_codedeploy/types/delete_deployment_group_output.py +42 -0
- capo_codedeploy/types/delete_git_hub_account_token_input.py +30 -0
- capo_codedeploy/types/delete_git_hub_account_token_output.py +30 -0
- capo_codedeploy/types/delete_resources_by_external_id_input.py +28 -0
- capo_codedeploy/types/delete_resources_by_external_id_output.py +18 -0
- capo_codedeploy/types/deployment_config_id.py +5 -0
- capo_codedeploy/types/deployment_config_info.py +134 -0
- capo_codedeploy/types/deployment_config_name.py +5 -0
- capo_codedeploy/types/deployment_configs_list.py +19 -0
- capo_codedeploy/types/deployment_creator.py +23 -0
- capo_codedeploy/types/deployment_group_id.py +5 -0
- capo_codedeploy/types/deployment_group_info.py +420 -0
- capo_codedeploy/types/deployment_group_info_list.py +35 -0
- capo_codedeploy/types/deployment_group_name.py +5 -0
- capo_codedeploy/types/deployment_groups_list.py +19 -0
- capo_codedeploy/types/deployment_id.py +5 -0
- capo_codedeploy/types/deployment_info.py +499 -0
- capo_codedeploy/types/deployment_option.py +17 -0
- capo_codedeploy/types/deployment_overview.py +64 -0
- capo_codedeploy/types/deployment_ready_action.py +17 -0
- capo_codedeploy/types/deployment_ready_option.py +50 -0
- capo_codedeploy/types/deployment_status.py +23 -0
- capo_codedeploy/types/deployment_status_list.py +33 -0
- capo_codedeploy/types/deployment_status_message_list.py +19 -0
- capo_codedeploy/types/deployment_style.py +61 -0
- capo_codedeploy/types/deployment_target.py +115 -0
- capo_codedeploy/types/deployment_target_list.py +33 -0
- capo_codedeploy/types/deployment_target_type.py +19 -0
- capo_codedeploy/types/deployment_type.py +17 -0
- capo_codedeploy/types/deployment_wait_type.py +17 -0
- capo_codedeploy/types/deployments_info_list.py +31 -0
- capo_codedeploy/types/deployments_list.py +17 -0
- capo_codedeploy/types/deregister_on_premises_instance_input.py +33 -0
- capo_codedeploy/types/description.py +5 -0
- capo_codedeploy/types/diagnostics.py +63 -0
- capo_codedeploy/types/duration.py +5 -0
- capo_codedeploy/types/e_tag.py +5 -0
- capo_codedeploy/types/ec2_tag_filter.py +52 -0
- capo_codedeploy/types/ec2_tag_filter_list.py +29 -0
- capo_codedeploy/types/ec2_tag_filter_type.py +18 -0
- capo_codedeploy/types/ec2_tag_set.py +42 -0
- capo_codedeploy/types/ec2_tag_set_list.py +35 -0
- capo_codedeploy/types/ecs_cluster_name.py +5 -0
- capo_codedeploy/types/ecs_service.py +35 -0
- capo_codedeploy/types/ecs_service_list.py +29 -0
- capo_codedeploy/types/ecs_service_name.py +5 -0
- capo_codedeploy/types/ecs_target.py +114 -0
- capo_codedeploy/types/ecs_task_set.py +111 -0
- capo_codedeploy/types/ecs_task_set_count.py +5 -0
- capo_codedeploy/types/ecs_task_set_identifier.py +5 -0
- capo_codedeploy/types/ecs_task_set_list.py +29 -0
- capo_codedeploy/types/ecs_task_set_status.py +5 -0
- capo_codedeploy/types/elb_info.py +28 -0
- capo_codedeploy/types/elb_info_list.py +29 -0
- capo_codedeploy/types/elb_name.py +5 -0
- capo_codedeploy/types/error_code.py +49 -0
- capo_codedeploy/types/error_information.py +43 -0
- capo_codedeploy/types/error_message.py +5 -0
- capo_codedeploy/types/external_id.py +5 -0
- capo_codedeploy/types/file_exists_behavior.py +18 -0
- capo_codedeploy/types/filter_value.py +5 -0
- capo_codedeploy/types/filter_value_list.py +17 -0
- capo_codedeploy/types/generic_revision_info.py +96 -0
- capo_codedeploy/types/get_application_input.py +31 -0
- capo_codedeploy/types/get_application_output.py +40 -0
- capo_codedeploy/types/get_application_revision_input.py +51 -0
- capo_codedeploy/types/get_application_revision_output.py +70 -0
- capo_codedeploy/types/get_deployment_config_input.py +35 -0
- capo_codedeploy/types/get_deployment_config_output.py +42 -0
- capo_codedeploy/types/get_deployment_group_input.py +43 -0
- capo_codedeploy/types/get_deployment_group_output.py +42 -0
- capo_codedeploy/types/get_deployment_input.py +31 -0
- capo_codedeploy/types/get_deployment_instance_input.py +39 -0
- capo_codedeploy/types/get_deployment_instance_output.py +42 -0
- capo_codedeploy/types/get_deployment_output.py +40 -0
- capo_codedeploy/types/get_deployment_target_input.py +39 -0
- capo_codedeploy/types/get_deployment_target_output.py +42 -0
- capo_codedeploy/types/get_on_premises_instance_input.py +31 -0
- capo_codedeploy/types/get_on_premises_instance_output.py +40 -0
- capo_codedeploy/types/git_hub_account_token_name.py +5 -0
- capo_codedeploy/types/git_hub_account_token_name_list.py +19 -0
- capo_codedeploy/types/git_hub_location.py +35 -0
- capo_codedeploy/types/green_fleet_provisioning_action.py +17 -0
- capo_codedeploy/types/green_fleet_provisioning_option.py +42 -0
- capo_codedeploy/types/iam_session_arn.py +5 -0
- capo_codedeploy/types/iam_user_arn.py +5 -0
- capo_codedeploy/types/instance_action.py +17 -0
- capo_codedeploy/types/instance_arn.py +5 -0
- capo_codedeploy/types/instance_count.py +5 -0
- capo_codedeploy/types/instance_id.py +5 -0
- capo_codedeploy/types/instance_info.py +95 -0
- capo_codedeploy/types/instance_info_list.py +29 -0
- capo_codedeploy/types/instance_name.py +5 -0
- capo_codedeploy/types/instance_name_list.py +17 -0
- capo_codedeploy/types/instance_status.py +22 -0
- capo_codedeploy/types/instance_status_list.py +31 -0
- capo_codedeploy/types/instance_summary.py +107 -0
- capo_codedeploy/types/instance_summary_list.py +33 -0
- capo_codedeploy/types/instance_target.py +112 -0
- capo_codedeploy/types/instance_type.py +17 -0
- capo_codedeploy/types/instance_type_list.py +29 -0
- capo_codedeploy/types/instances_list.py +17 -0
- capo_codedeploy/types/key.py +5 -0
- capo_codedeploy/types/lambda_function_alias.py +5 -0
- capo_codedeploy/types/lambda_function_info.py +69 -0
- capo_codedeploy/types/lambda_function_name.py +5 -0
- capo_codedeploy/types/lambda_target.py +114 -0
- capo_codedeploy/types/last_deployment_info.py +74 -0
- capo_codedeploy/types/lifecycle_error_code.py +21 -0
- capo_codedeploy/types/lifecycle_event.py +95 -0
- capo_codedeploy/types/lifecycle_event_hook_execution_id.py +5 -0
- capo_codedeploy/types/lifecycle_event_list.py +31 -0
- capo_codedeploy/types/lifecycle_event_name.py +5 -0
- capo_codedeploy/types/lifecycle_event_status.py +21 -0
- capo_codedeploy/types/lifecycle_message.py +5 -0
- capo_codedeploy/types/list_application_revisions_input.py +111 -0
- capo_codedeploy/types/list_application_revisions_output.py +49 -0
- capo_codedeploy/types/list_applications_input.py +28 -0
- capo_codedeploy/types/list_applications_output.py +49 -0
- capo_codedeploy/types/list_deployment_configs_input.py +28 -0
- capo_codedeploy/types/list_deployment_configs_output.py +49 -0
- capo_codedeploy/types/list_deployment_groups_input.py +40 -0
- capo_codedeploy/types/list_deployment_groups_output.py +58 -0
- capo_codedeploy/types/list_deployment_instances_input.py +82 -0
- capo_codedeploy/types/list_deployment_instances_output.py +47 -0
- capo_codedeploy/types/list_deployment_targets_input.py +57 -0
- capo_codedeploy/types/list_deployment_targets_output.py +45 -0
- capo_codedeploy/types/list_deployments_input.py +93 -0
- capo_codedeploy/types/list_deployments_output.py +47 -0
- capo_codedeploy/types/list_git_hub_account_token_names_input.py +28 -0
- capo_codedeploy/types/list_git_hub_account_token_names_output.py +49 -0
- capo_codedeploy/types/list_on_premises_instances_input.py +68 -0
- capo_codedeploy/types/list_on_premises_instances_output.py +49 -0
- capo_codedeploy/types/list_state_filter_action.py +18 -0
- capo_codedeploy/types/list_tags_for_resource_input.py +38 -0
- capo_codedeploy/types/list_tags_for_resource_output.py +43 -0
- capo_codedeploy/types/listener_arn.py +5 -0
- capo_codedeploy/types/listener_arn_list.py +17 -0
- capo_codedeploy/types/load_balancer_info.py +80 -0
- capo_codedeploy/types/log_tail.py +5 -0
- capo_codedeploy/types/message.py +5 -0
- capo_codedeploy/types/minimum_healthy_hosts.py +50 -0
- capo_codedeploy/types/minimum_healthy_hosts_per_zone.py +50 -0
- capo_codedeploy/types/minimum_healthy_hosts_per_zone_type.py +17 -0
- capo_codedeploy/types/minimum_healthy_hosts_per_zone_value.py +5 -0
- capo_codedeploy/types/minimum_healthy_hosts_type.py +17 -0
- capo_codedeploy/types/minimum_healthy_hosts_value.py +5 -0
- capo_codedeploy/types/next_token.py +5 -0
- capo_codedeploy/types/nullable_boolean.py +5 -0
- capo_codedeploy/types/on_premises_tag_set.py +42 -0
- capo_codedeploy/types/on_premises_tag_set_list.py +31 -0
- capo_codedeploy/types/outdated_instances_strategy.py +17 -0
- capo_codedeploy/types/percentage.py +5 -0
- capo_codedeploy/types/put_lifecycle_event_hook_execution_status_input.py +60 -0
- capo_codedeploy/types/put_lifecycle_event_hook_execution_status_output.py +32 -0
- capo_codedeploy/types/raw_string.py +35 -0
- capo_codedeploy/types/raw_string_content.py +5 -0
- capo_codedeploy/types/raw_string_sha256.py +5 -0
- capo_codedeploy/types/register_application_revision_input.py +58 -0
- capo_codedeploy/types/register_on_premises_instance_input.py +47 -0
- capo_codedeploy/types/registration_status.py +17 -0
- capo_codedeploy/types/related_deployments.py +55 -0
- capo_codedeploy/types/remove_tags_from_on_premises_instances_input.py +61 -0
- capo_codedeploy/types/repository.py +5 -0
- capo_codedeploy/types/revision_info.py +63 -0
- capo_codedeploy/types/revision_info_list.py +29 -0
- capo_codedeploy/types/revision_location.py +114 -0
- capo_codedeploy/types/revision_location_list.py +33 -0
- capo_codedeploy/types/revision_location_type.py +19 -0
- capo_codedeploy/types/role.py +5 -0
- capo_codedeploy/types/rollback_info.py +49 -0
- capo_codedeploy/types/s3_bucket.py +5 -0
- capo_codedeploy/types/s3_key.py +5 -0
- capo_codedeploy/types/s3_location.py +64 -0
- capo_codedeploy/types/script_name.py +5 -0
- capo_codedeploy/types/skip_wait_time_for_instance_termination_input.py +28 -0
- capo_codedeploy/types/sort_order.py +17 -0
- capo_codedeploy/types/stop_deployment_input.py +40 -0
- capo_codedeploy/types/stop_deployment_output.py +43 -0
- capo_codedeploy/types/stop_status.py +17 -0
- capo_codedeploy/types/tag.py +35 -0
- capo_codedeploy/types/tag_filter.py +50 -0
- capo_codedeploy/types/tag_filter_list.py +29 -0
- capo_codedeploy/types/tag_filter_type.py +18 -0
- capo_codedeploy/types/tag_key_list.py +17 -0
- capo_codedeploy/types/tag_list.py +29 -0
- capo_codedeploy/types/tag_resource_input.py +45 -0
- capo_codedeploy/types/tag_resource_output.py +18 -0
- capo_codedeploy/types/target_arn.py +5 -0
- capo_codedeploy/types/target_filter_name.py +17 -0
- capo_codedeploy/types/target_filters.py +40 -0
- capo_codedeploy/types/target_group_info.py +28 -0
- capo_codedeploy/types/target_group_info_list.py +33 -0
- capo_codedeploy/types/target_group_name.py +5 -0
- capo_codedeploy/types/target_group_pair_info.py +79 -0
- capo_codedeploy/types/target_group_pair_info_list.py +35 -0
- capo_codedeploy/types/target_id.py +5 -0
- capo_codedeploy/types/target_id_list.py +17 -0
- capo_codedeploy/types/target_instances.py +78 -0
- capo_codedeploy/types/target_label.py +17 -0
- capo_codedeploy/types/target_status.py +22 -0
- capo_codedeploy/types/time.py +20 -0
- capo_codedeploy/types/time_based_canary.py +37 -0
- capo_codedeploy/types/time_based_linear.py +37 -0
- capo_codedeploy/types/time_range.py +50 -0
- capo_codedeploy/types/timestamp.py +20 -0
- capo_codedeploy/types/traffic_route.py +42 -0
- capo_codedeploy/types/traffic_routing_config.py +80 -0
- capo_codedeploy/types/traffic_routing_type.py +18 -0
- capo_codedeploy/types/traffic_weight.py +5 -0
- capo_codedeploy/types/trigger_config.py +58 -0
- capo_codedeploy/types/trigger_config_list.py +31 -0
- capo_codedeploy/types/trigger_event_type.py +25 -0
- capo_codedeploy/types/trigger_event_type_list.py +35 -0
- capo_codedeploy/types/trigger_name.py +5 -0
- capo_codedeploy/types/trigger_target_arn.py +5 -0
- capo_codedeploy/types/untag_resource_input.py +47 -0
- capo_codedeploy/types/untag_resource_output.py +18 -0
- capo_codedeploy/types/update_application_input.py +38 -0
- capo_codedeploy/types/update_deployment_group_input.py +343 -0
- capo_codedeploy/types/update_deployment_group_output.py +42 -0
- capo_codedeploy/types/value.py +5 -0
- capo_codedeploy/types/version.py +5 -0
- capo_codedeploy/types/version_id.py +5 -0
- capo_codedeploy/types/wait_time_in_mins.py +5 -0
- capo_codedeploy/types/wait_time_in_seconds.py +5 -0
- capo_codedeploy/types/zonal_config.py +63 -0
- capo_codedeploy-0.1.0.dist-info/METADATA +102 -0
- capo_codedeploy-0.1.0.dist-info/RECORD +472 -0
- capo_codedeploy-0.1.0.dist-info/WHEEL +5 -0
- capo_codedeploy-0.1.0.dist-info/licenses/LICENSE +21 -0
- capo_codedeploy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
from zapros import AsyncClient, Client, ZaprosError
|
|
15
|
+
|
|
16
|
+
from capo_codedeploy._auth._identity import (
|
|
17
|
+
Credentials,
|
|
18
|
+
Identity,
|
|
19
|
+
)
|
|
20
|
+
from capo_codedeploy._services._aws_config import (
|
|
21
|
+
_load_profile,
|
|
22
|
+
active_profile,
|
|
23
|
+
config_file,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# The SSO, assume-role and web-identity providers call other AWS services, so
|
|
27
|
+
# they need the `sso` extra: `pip install capo-codedeploy[sso]`. Without it the imports
|
|
28
|
+
# below stay `None` and only those providers fail — see `require_dependency`.
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
import capo_sso
|
|
31
|
+
import capo_sso.errors
|
|
32
|
+
import capo_sso.types.get_role_credentials_response
|
|
33
|
+
import capo_sso_oidc
|
|
34
|
+
import capo_sso_oidc.errors
|
|
35
|
+
import capo_sso_oidc.types.create_token_response
|
|
36
|
+
import capo_sts
|
|
37
|
+
import capo_sts.types.credentials
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
import capo_sso
|
|
41
|
+
import capo_sso.errors
|
|
42
|
+
except ImportError:
|
|
43
|
+
capo_sso = None
|
|
44
|
+
try:
|
|
45
|
+
import capo_sso_oidc
|
|
46
|
+
import capo_sso_oidc.errors
|
|
47
|
+
except ImportError:
|
|
48
|
+
capo_sso_oidc = None
|
|
49
|
+
try:
|
|
50
|
+
import capo_sts
|
|
51
|
+
except ImportError:
|
|
52
|
+
capo_sts = None
|
|
53
|
+
|
|
54
|
+
# refresh an SSO token this long before it actually expires
|
|
55
|
+
SSO_TOKEN_REFRESH_WINDOW = timedelta(minutes=5)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IdentityNotFound(Exception):
|
|
59
|
+
"""Raised when a provider cannot resolve an identity. Chain continues."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MissingDependencyError(Exception):
|
|
63
|
+
"""Raised when a provider needs the `sso` extra but it is not installed."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SSOError(Exception):
|
|
67
|
+
"""Raised when SSO is configured but unusable. Chain stops."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AssumeRoleError(Exception):
|
|
71
|
+
"""Raised when a `role_arn` profile is configured but unusable. Chain stops."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def require_dependency(module: object | None, package: str) -> None:
|
|
75
|
+
if module is None:
|
|
76
|
+
raise MissingDependencyError(
|
|
77
|
+
f"{package} is required for this credentials provider but is not "
|
|
78
|
+
"installed; reinstall with the sso feature enabled: capo-codedeploy[sso]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
IdentityT = TypeVar("IdentityT", bound="Identity")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class IdentityProvider(Generic[IdentityT]):
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def resolve_identity(self) -> IdentityT:
|
|
88
|
+
raise NotImplementedError
|
|
89
|
+
|
|
90
|
+
async def aresolve_identity(self) -> IdentityT:
|
|
91
|
+
# default: no network I/O, reuse the sync resolution
|
|
92
|
+
return self.resolve_identity()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ChainedProvider(IdentityProvider[IdentityT]):
|
|
96
|
+
"""Try each provider in order; first non-`IdentityNotFound` wins."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, *providers: IdentityProvider[IdentityT]) -> None:
|
|
99
|
+
if not providers:
|
|
100
|
+
raise ValueError("ChainedProvider requires at least one provider")
|
|
101
|
+
self._providers = providers
|
|
102
|
+
|
|
103
|
+
def resolve_identity(self) -> IdentityT:
|
|
104
|
+
errors: list[str] = []
|
|
105
|
+
for p in self._providers:
|
|
106
|
+
try:
|
|
107
|
+
return p.resolve_identity()
|
|
108
|
+
except IdentityNotFound as e:
|
|
109
|
+
errors.append(f"{type(p).__name__}: {e}")
|
|
110
|
+
raise IdentityNotFound("no provider succeeded: " + "; ".join(errors))
|
|
111
|
+
|
|
112
|
+
async def aresolve_identity(self) -> IdentityT:
|
|
113
|
+
errors: list[str] = []
|
|
114
|
+
for p in self._providers:
|
|
115
|
+
try:
|
|
116
|
+
return await p.aresolve_identity()
|
|
117
|
+
except IdentityNotFound as e:
|
|
118
|
+
errors.append(f"{type(p).__name__}: {e}")
|
|
119
|
+
raise IdentityNotFound("no provider succeeded: " + "; ".join(errors))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CachedProvider(IdentityProvider[IdentityT]):
|
|
123
|
+
"""Cache an identity until its `expiration` (minus skew) elapses."""
|
|
124
|
+
|
|
125
|
+
_SKEW_SECONDS = 60
|
|
126
|
+
|
|
127
|
+
def __init__(self, inner: IdentityProvider[IdentityT]) -> None:
|
|
128
|
+
self._inner = inner
|
|
129
|
+
self._cached: IdentityT | None = None
|
|
130
|
+
|
|
131
|
+
def resolve_identity(self) -> IdentityT:
|
|
132
|
+
if self._cached is not None and not self._expired(self._cached):
|
|
133
|
+
return self._cached
|
|
134
|
+
self._cached = self._inner.resolve_identity()
|
|
135
|
+
return self._cached
|
|
136
|
+
|
|
137
|
+
async def aresolve_identity(self) -> IdentityT:
|
|
138
|
+
if self._cached is not None and not self._expired(self._cached):
|
|
139
|
+
return self._cached
|
|
140
|
+
self._cached = await self._inner.aresolve_identity()
|
|
141
|
+
return self._cached
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def _expired(cls, ident: Identity) -> bool:
|
|
145
|
+
exp = ident.get("expiration")
|
|
146
|
+
if exp is None:
|
|
147
|
+
return False
|
|
148
|
+
return (exp - datetime.now(timezone.utc)).total_seconds() <= cls._SKEW_SECONDS
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class CredentialsProvider(IdentityProvider[Credentials]):
|
|
152
|
+
"""Base class for providers that resolve AWS `Credentials`."""
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def resolve_identity(self) -> Credentials:
|
|
156
|
+
raise NotImplementedError
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class StaticAwsCredentialsProvider(CredentialsProvider):
|
|
160
|
+
def __init__(self, credentials: Credentials) -> None:
|
|
161
|
+
self._credentials = credentials
|
|
162
|
+
|
|
163
|
+
def resolve_identity(self) -> Credentials:
|
|
164
|
+
return self._credentials
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class EnvCredentialsProvider(CredentialsProvider):
|
|
168
|
+
"""Read AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN."""
|
|
169
|
+
|
|
170
|
+
def resolve_identity(self) -> Credentials:
|
|
171
|
+
ak = os.environ.get("AWS_ACCESS_KEY_ID")
|
|
172
|
+
sk = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
173
|
+
if not ak or not sk:
|
|
174
|
+
raise IdentityNotFound("AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY unset")
|
|
175
|
+
out: Credentials = {"access_key": ak, "secret_key": sk}
|
|
176
|
+
token = os.environ.get("AWS_SESSION_TOKEN")
|
|
177
|
+
if token:
|
|
178
|
+
out["session_token"] = token
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ProfileCredentialsProvider(CredentialsProvider):
|
|
183
|
+
"""Read ~/.aws/credentials and ~/.aws/config for the active profile."""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self, credentials_file: Path | None = None, profile: str | None = None
|
|
187
|
+
) -> None:
|
|
188
|
+
self._profile = profile or active_profile()
|
|
189
|
+
self._cred_file = credentials_file or Path(
|
|
190
|
+
os.environ.get("AWS_SHARED_CREDENTIALS_FILE")
|
|
191
|
+
or Path.home() / ".aws" / "credentials"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def resolve_identity(self) -> Credentials:
|
|
195
|
+
section = self._load_section()
|
|
196
|
+
ak = section.get("aws_access_key_id")
|
|
197
|
+
sk = section.get("aws_secret_access_key")
|
|
198
|
+
if not ak or not sk:
|
|
199
|
+
raise IdentityNotFound(
|
|
200
|
+
f"profile {self._profile!r}: missing aws_access_key_id/aws_secret_access_key"
|
|
201
|
+
)
|
|
202
|
+
out: Credentials = {"access_key": ak, "secret_key": sk}
|
|
203
|
+
token = section.get("aws_session_token")
|
|
204
|
+
if token:
|
|
205
|
+
out["session_token"] = token
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
def _load_section(self) -> dict[str, str]:
|
|
209
|
+
# config-file profile reuses the loader from _services/_aws_config
|
|
210
|
+
merged, _ = _load_profile(self._profile)
|
|
211
|
+
if self._cred_file.is_file():
|
|
212
|
+
cfg = configparser.ConfigParser(interpolation=None)
|
|
213
|
+
cfg.read(self._cred_file)
|
|
214
|
+
if cfg.has_section(self._profile):
|
|
215
|
+
merged.update(dict(cfg.items(self._profile)))
|
|
216
|
+
if not merged:
|
|
217
|
+
raise IdentityNotFound(
|
|
218
|
+
f"profile {self._profile!r} not found in credentials/config files"
|
|
219
|
+
)
|
|
220
|
+
return merged
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class EcsContainerCredentialsProvider(CredentialsProvider):
|
|
224
|
+
"""Resolve credentials from the ECS/EKS container credentials endpoint."""
|
|
225
|
+
|
|
226
|
+
def __init__(self, client: Client | AsyncClient) -> None:
|
|
227
|
+
self._client = client
|
|
228
|
+
|
|
229
|
+
def resolve_identity(self) -> Credentials:
|
|
230
|
+
if isinstance(self._client, AsyncClient):
|
|
231
|
+
raise TypeError(
|
|
232
|
+
"EcsContainerCredentialsProvider configured with AsyncClient; use aresolve_identity"
|
|
233
|
+
)
|
|
234
|
+
url, headers = self._request_args()
|
|
235
|
+
resp = self._client.get(url, headers=headers)
|
|
236
|
+
if resp.status < 200 or resp.status >= 300:
|
|
237
|
+
raise IdentityNotFound(
|
|
238
|
+
f"ECS credentials endpoint returned status {resp.status}"
|
|
239
|
+
)
|
|
240
|
+
return _credentials_from_json(resp.json)
|
|
241
|
+
|
|
242
|
+
async def aresolve_identity(self) -> Credentials:
|
|
243
|
+
if not isinstance(self._client, AsyncClient):
|
|
244
|
+
raise TypeError(
|
|
245
|
+
"EcsContainerCredentialsProvider configured with sync Client; use resolve_identity"
|
|
246
|
+
)
|
|
247
|
+
url, headers = self._request_args()
|
|
248
|
+
resp = await self._client.get(url, headers=headers)
|
|
249
|
+
if resp.status < 200 or resp.status >= 300:
|
|
250
|
+
raise IdentityNotFound(
|
|
251
|
+
f"ECS credentials endpoint returned status {resp.status}"
|
|
252
|
+
)
|
|
253
|
+
return _credentials_from_json(resp.json)
|
|
254
|
+
|
|
255
|
+
def _request_args(self) -> tuple[str, dict[str, str]]:
|
|
256
|
+
relative = os.environ.get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
|
257
|
+
full = os.environ.get("AWS_CONTAINER_CREDENTIALS_FULL_URI")
|
|
258
|
+
if relative:
|
|
259
|
+
url = "http://169.254.170.2" + relative
|
|
260
|
+
elif full:
|
|
261
|
+
url = full
|
|
262
|
+
else:
|
|
263
|
+
raise IdentityNotFound("no ECS container credentials env var set")
|
|
264
|
+
headers: dict[str, str] = {}
|
|
265
|
+
token = os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN")
|
|
266
|
+
token_file = os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
|
|
267
|
+
if token_file:
|
|
268
|
+
token = Path(token_file).read_text().strip()
|
|
269
|
+
if token:
|
|
270
|
+
headers["Authorization"] = token
|
|
271
|
+
return url, headers
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class Ec2InstanceMetadataProvider(CredentialsProvider):
|
|
275
|
+
"""Resolve credentials from the EC2 Instance Metadata Service (IMDSv2)."""
|
|
276
|
+
|
|
277
|
+
_BASE = "http://169.254.169.254"
|
|
278
|
+
_TOKEN_PATH = "/latest/api/token"
|
|
279
|
+
_CREDS_PATH = "/latest/meta-data/iam/security-credentials/"
|
|
280
|
+
|
|
281
|
+
def __init__(self, client: Client | AsyncClient) -> None:
|
|
282
|
+
self._client = client
|
|
283
|
+
|
|
284
|
+
def resolve_identity(self) -> Credentials:
|
|
285
|
+
if isinstance(self._client, AsyncClient):
|
|
286
|
+
raise TypeError(
|
|
287
|
+
"Ec2InstanceMetadataProvider configured with AsyncClient; use aresolve_identity"
|
|
288
|
+
)
|
|
289
|
+
if os.environ.get("AWS_EC2_METADATA_DISABLED", "").strip().lower() in (
|
|
290
|
+
"true",
|
|
291
|
+
"1",
|
|
292
|
+
):
|
|
293
|
+
raise IdentityNotFound("IMDS disabled via AWS_EC2_METADATA_DISABLED")
|
|
294
|
+
try:
|
|
295
|
+
token_resp = self._client.put(
|
|
296
|
+
self._BASE + self._TOKEN_PATH,
|
|
297
|
+
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
|
|
298
|
+
body=b"",
|
|
299
|
+
)
|
|
300
|
+
auth = {"X-aws-ec2-metadata-token": token_resp.text}
|
|
301
|
+
role_resp = self._client.get(self._BASE + self._CREDS_PATH, headers=auth)
|
|
302
|
+
role = role_resp.text.strip()
|
|
303
|
+
creds_resp = self._client.get(
|
|
304
|
+
self._BASE + self._CREDS_PATH + role, headers=auth
|
|
305
|
+
)
|
|
306
|
+
except ZaprosError as e:
|
|
307
|
+
raise IdentityNotFound(f"IMDS request failed: {e}")
|
|
308
|
+
if creds_resp.status < 200 or creds_resp.status >= 300:
|
|
309
|
+
raise IdentityNotFound(f"IMDS returned status {creds_resp.status}")
|
|
310
|
+
return _credentials_from_json(creds_resp.json)
|
|
311
|
+
|
|
312
|
+
async def aresolve_identity(self) -> Credentials:
|
|
313
|
+
if not isinstance(self._client, AsyncClient):
|
|
314
|
+
raise TypeError(
|
|
315
|
+
"Ec2InstanceMetadataProvider configured with sync Client; use resolve_identity"
|
|
316
|
+
)
|
|
317
|
+
if os.environ.get("AWS_EC2_METADATA_DISABLED", "").strip().lower() in (
|
|
318
|
+
"true",
|
|
319
|
+
"1",
|
|
320
|
+
):
|
|
321
|
+
raise IdentityNotFound("IMDS disabled via AWS_EC2_METADATA_DISABLED")
|
|
322
|
+
try:
|
|
323
|
+
token_resp = await self._client.put(
|
|
324
|
+
self._BASE + self._TOKEN_PATH,
|
|
325
|
+
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
|
|
326
|
+
body=b"",
|
|
327
|
+
)
|
|
328
|
+
auth = {"X-aws-ec2-metadata-token": token_resp.text}
|
|
329
|
+
role_resp = await self._client.get(
|
|
330
|
+
self._BASE + self._CREDS_PATH, headers=auth
|
|
331
|
+
)
|
|
332
|
+
role = role_resp.text.strip()
|
|
333
|
+
creds_resp = await self._client.get(
|
|
334
|
+
self._BASE + self._CREDS_PATH + role, headers=auth
|
|
335
|
+
)
|
|
336
|
+
except ZaprosError as e:
|
|
337
|
+
raise IdentityNotFound(f"IMDS request failed: {e}")
|
|
338
|
+
if creds_resp.status < 200 or creds_resp.status >= 300:
|
|
339
|
+
raise IdentityNotFound(f"IMDS returned status {creds_resp.status}")
|
|
340
|
+
return _credentials_from_json(creds_resp.json)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass(frozen=True)
|
|
344
|
+
class SsoConfig:
|
|
345
|
+
start_url: str
|
|
346
|
+
region: str
|
|
347
|
+
account_id: str
|
|
348
|
+
role_name: str
|
|
349
|
+
session_name: str | None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class SsoCredentialsProvider(CredentialsProvider):
|
|
353
|
+
"""Resolve credentials from AWS IAM Identity Center (SSO).
|
|
354
|
+
|
|
355
|
+
Reads the profile's ``sso_*`` settings, loads the access token that
|
|
356
|
+
``aws sso login`` cached under ``~/.aws/sso/cache`` (refreshing it through
|
|
357
|
+
SSO-OIDC when it is about to expire) and exchanges it for short-term
|
|
358
|
+
credentials through the SSO ``GetRoleCredentials`` API.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(
|
|
362
|
+
self, client: Client | AsyncClient, profile: str | None = None
|
|
363
|
+
) -> None:
|
|
364
|
+
self._client = client
|
|
365
|
+
self._profile = profile
|
|
366
|
+
|
|
367
|
+
def resolve_identity(self) -> Credentials:
|
|
368
|
+
if isinstance(self._client, AsyncClient):
|
|
369
|
+
raise TypeError(
|
|
370
|
+
"SsoCredentialsProvider configured with AsyncClient; use aresolve_identity"
|
|
371
|
+
)
|
|
372
|
+
# resolve the config first so an unconfigured profile just skips SSO
|
|
373
|
+
config = load_sso_config(self._profile)
|
|
374
|
+
require_dependency(capo_sso, "capo-sso")
|
|
375
|
+
token = load_sso_token(config, self._client)
|
|
376
|
+
sso = capo_sso.SSOClient(http_handler=self._client.handler)
|
|
377
|
+
try:
|
|
378
|
+
response = sso.get_role_credentials(
|
|
379
|
+
config.role_name,
|
|
380
|
+
config.account_id,
|
|
381
|
+
token,
|
|
382
|
+
config_overrides={
|
|
383
|
+
"region": config.region,
|
|
384
|
+
"credentials_provider": None,
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
except capo_sso.errors.UnauthorizedException as e:
|
|
388
|
+
raise SSOError(f"SSO token rejected; run `aws sso login`: {e}")
|
|
389
|
+
return credentials_from_role_credentials(response)
|
|
390
|
+
|
|
391
|
+
async def aresolve_identity(self) -> Credentials:
|
|
392
|
+
if not isinstance(self._client, AsyncClient):
|
|
393
|
+
raise TypeError(
|
|
394
|
+
"SsoCredentialsProvider configured with sync Client; use resolve_identity"
|
|
395
|
+
)
|
|
396
|
+
# resolve the config first so an unconfigured profile just skips SSO
|
|
397
|
+
config = load_sso_config(self._profile)
|
|
398
|
+
require_dependency(capo_sso, "capo-sso")
|
|
399
|
+
token = await aload_sso_token(config, self._client)
|
|
400
|
+
sso = capo_sso.AsyncSSOClient(http_handler=self._client.handler)
|
|
401
|
+
try:
|
|
402
|
+
response = await sso.get_role_credentials(
|
|
403
|
+
config.role_name,
|
|
404
|
+
config.account_id,
|
|
405
|
+
token,
|
|
406
|
+
config_overrides={
|
|
407
|
+
"region": config.region,
|
|
408
|
+
"credentials_provider": None,
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
except capo_sso.errors.UnauthorizedException as e:
|
|
412
|
+
raise SSOError(f"SSO token rejected; run `aws sso login`: {e}")
|
|
413
|
+
return credentials_from_role_credentials(response)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def load_config_section(name: str) -> dict[str, str]:
|
|
417
|
+
cfg_file = config_file()
|
|
418
|
+
if not cfg_file.is_file():
|
|
419
|
+
return {}
|
|
420
|
+
cfg = configparser.ConfigParser(interpolation=None)
|
|
421
|
+
cfg.read(cfg_file)
|
|
422
|
+
return dict(cfg.items(name)) if cfg.has_section(name) else {}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def load_sso_config(profile_name: str | None = None) -> SsoConfig:
|
|
426
|
+
profile, _ = _load_profile(profile_name)
|
|
427
|
+
session_name = profile.get("sso_session")
|
|
428
|
+
if session_name is None and not any(k.startswith("sso_") for k in profile):
|
|
429
|
+
raise IdentityNotFound("profile has no sso_* settings")
|
|
430
|
+
if session_name is not None:
|
|
431
|
+
# `sso_session` moves the portal settings into an [sso-session NAME] section
|
|
432
|
+
session = load_config_section(f"sso-session {session_name}")
|
|
433
|
+
start_url = session.get("sso_start_url")
|
|
434
|
+
region = session.get("sso_region")
|
|
435
|
+
else:
|
|
436
|
+
start_url = profile.get("sso_start_url")
|
|
437
|
+
region = profile.get("sso_region")
|
|
438
|
+
account_id = profile.get("sso_account_id")
|
|
439
|
+
role_name = profile.get("sso_role_name")
|
|
440
|
+
missing = [
|
|
441
|
+
name
|
|
442
|
+
for name, value in (
|
|
443
|
+
("sso_start_url", start_url),
|
|
444
|
+
("sso_region", region),
|
|
445
|
+
("sso_account_id", account_id),
|
|
446
|
+
("sso_role_name", role_name),
|
|
447
|
+
)
|
|
448
|
+
if not value
|
|
449
|
+
]
|
|
450
|
+
if missing:
|
|
451
|
+
raise SSOError("incomplete sso configuration: missing " + ", ".join(missing))
|
|
452
|
+
assert start_url and region and account_id and role_name # narrowed by `missing`
|
|
453
|
+
return SsoConfig(
|
|
454
|
+
start_url=start_url,
|
|
455
|
+
region=region,
|
|
456
|
+
account_id=account_id,
|
|
457
|
+
role_name=role_name,
|
|
458
|
+
session_name=session_name,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def read_sso_token_cache(config: SsoConfig) -> tuple[Path, dict[str, object]]:
|
|
463
|
+
# cache file is sha1 of the session name (new format) or the start url (legacy)
|
|
464
|
+
key = config.session_name or config.start_url
|
|
465
|
+
digest = hashlib.sha1(key.encode("utf-8")).hexdigest()
|
|
466
|
+
path = Path.home() / ".aws" / "sso" / "cache" / f"{digest}.json"
|
|
467
|
+
if not path.is_file():
|
|
468
|
+
raise SSOError(f"no cached SSO token at {path}; run `aws sso login`")
|
|
469
|
+
data = json.loads(path.read_text())
|
|
470
|
+
if not isinstance(data.get("accessToken"), str) or not isinstance(
|
|
471
|
+
data.get("expiresAt"), str
|
|
472
|
+
):
|
|
473
|
+
raise SSOError(f"SSO token cache {path} has no accessToken/expiresAt")
|
|
474
|
+
return path, data
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def unexpired_sso_token(data: dict[str, object], *, skew: timedelta) -> str | None:
|
|
478
|
+
"""The cached token if it is still good for at least ``skew``, else None."""
|
|
479
|
+
expires_at = parse_sso_expiry(str(data["expiresAt"]))
|
|
480
|
+
if expires_at - datetime.now(timezone.utc) <= skew:
|
|
481
|
+
return None
|
|
482
|
+
return str(data["accessToken"])
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def sso_refresh_args(data: dict[str, object]) -> tuple[str, str, str] | None:
|
|
486
|
+
"""Client registration + refresh token, if this entry can be refreshed.
|
|
487
|
+
|
|
488
|
+
Only tokens cached for an ``sso_session`` carry a client registration; the
|
|
489
|
+
legacy ``sso_start_url`` format has nothing to refresh with.
|
|
490
|
+
"""
|
|
491
|
+
client_id = data.get("clientId")
|
|
492
|
+
client_secret = data.get("clientSecret")
|
|
493
|
+
refresh_token = data.get("refreshToken")
|
|
494
|
+
if (
|
|
495
|
+
not isinstance(client_id, str)
|
|
496
|
+
or not isinstance(client_secret, str)
|
|
497
|
+
or not isinstance(refresh_token, str)
|
|
498
|
+
):
|
|
499
|
+
return None
|
|
500
|
+
registration_expires_at = data.get("registrationExpiresAt")
|
|
501
|
+
if isinstance(registration_expires_at, str):
|
|
502
|
+
if parse_sso_expiry(registration_expires_at) <= datetime.now(timezone.utc):
|
|
503
|
+
return None
|
|
504
|
+
return client_id, client_secret, refresh_token
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def load_sso_token(config: SsoConfig, client: Client) -> str:
|
|
508
|
+
path, data = read_sso_token_cache(config)
|
|
509
|
+
fresh = unexpired_sso_token(data, skew=SSO_TOKEN_REFRESH_WINDOW)
|
|
510
|
+
if fresh is not None:
|
|
511
|
+
return fresh
|
|
512
|
+
refresh_args = sso_refresh_args(data)
|
|
513
|
+
if refresh_args is not None:
|
|
514
|
+
require_dependency(capo_sso_oidc, "capo-sso-oidc")
|
|
515
|
+
oidc = capo_sso_oidc.SSOOIDCClient(http_handler=client.handler)
|
|
516
|
+
try:
|
|
517
|
+
response = oidc.create_token(
|
|
518
|
+
refresh_args[0],
|
|
519
|
+
refresh_args[1],
|
|
520
|
+
"refresh_token",
|
|
521
|
+
refresh_token=refresh_args[2],
|
|
522
|
+
config_overrides={
|
|
523
|
+
"region": config.region,
|
|
524
|
+
"credentials_provider": None,
|
|
525
|
+
},
|
|
526
|
+
)
|
|
527
|
+
except (capo_sso_oidc.errors.SSOOIDCError, ZaprosError):
|
|
528
|
+
return sso_token_or_error(data, path)
|
|
529
|
+
return store_refreshed_sso_token(path, data, response)
|
|
530
|
+
return sso_token_or_error(data, path)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async def aload_sso_token(config: SsoConfig, client: AsyncClient) -> str:
|
|
534
|
+
path, data = read_sso_token_cache(config)
|
|
535
|
+
fresh = unexpired_sso_token(data, skew=SSO_TOKEN_REFRESH_WINDOW)
|
|
536
|
+
if fresh is not None:
|
|
537
|
+
return fresh
|
|
538
|
+
refresh_args = sso_refresh_args(data)
|
|
539
|
+
if refresh_args is not None:
|
|
540
|
+
require_dependency(capo_sso_oidc, "capo-sso-oidc")
|
|
541
|
+
oidc = capo_sso_oidc.AsyncSSOOIDCClient(http_handler=client.handler)
|
|
542
|
+
try:
|
|
543
|
+
response = await oidc.create_token(
|
|
544
|
+
refresh_args[0],
|
|
545
|
+
refresh_args[1],
|
|
546
|
+
"refresh_token",
|
|
547
|
+
refresh_token=refresh_args[2],
|
|
548
|
+
config_overrides={
|
|
549
|
+
"region": config.region,
|
|
550
|
+
"credentials_provider": None,
|
|
551
|
+
},
|
|
552
|
+
)
|
|
553
|
+
except (capo_sso_oidc.errors.SSOOIDCError, ZaprosError):
|
|
554
|
+
return sso_token_or_error(data, path)
|
|
555
|
+
return store_refreshed_sso_token(path, data, response)
|
|
556
|
+
return sso_token_or_error(data, path)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def sso_token_or_error(data: dict[str, object], path: Path) -> str:
|
|
560
|
+
"""Fall back to the cached token when refreshing was impossible or failed."""
|
|
561
|
+
token = unexpired_sso_token(data, skew=timedelta(0))
|
|
562
|
+
if token is None:
|
|
563
|
+
raise SSOError(
|
|
564
|
+
f"cached SSO token in {path} expired at {data['expiresAt']}; "
|
|
565
|
+
"run `aws sso login`"
|
|
566
|
+
)
|
|
567
|
+
return token
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def store_refreshed_sso_token(
|
|
571
|
+
path: Path,
|
|
572
|
+
data: dict[str, object],
|
|
573
|
+
response: capo_sso_oidc.types.create_token_response.CreateTokenResponse,
|
|
574
|
+
) -> str:
|
|
575
|
+
access_token = response.get("access_token")
|
|
576
|
+
if not access_token:
|
|
577
|
+
raise SSOError("CreateToken response has no accessToken")
|
|
578
|
+
expires_at = datetime.now(timezone.utc) + timedelta(
|
|
579
|
+
seconds=response.get("expires_in", 0)
|
|
580
|
+
)
|
|
581
|
+
updated = dict(data)
|
|
582
|
+
updated["accessToken"] = access_token
|
|
583
|
+
updated["expiresAt"] = expires_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
584
|
+
refresh_token = response.get("refresh_token")
|
|
585
|
+
if refresh_token:
|
|
586
|
+
updated["refreshToken"] = refresh_token
|
|
587
|
+
path.write_text(json.dumps(updated))
|
|
588
|
+
path.chmod(0o600)
|
|
589
|
+
return access_token
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def parse_sso_expiry(value: str) -> datetime:
|
|
593
|
+
# the CLI writes either an ISO offset, a trailing 'Z', or a trailing 'UTC'
|
|
594
|
+
parsed = datetime.fromisoformat(value.removesuffix("UTC").removesuffix("Z"))
|
|
595
|
+
if parsed.tzinfo is None:
|
|
596
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
597
|
+
return parsed
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def credentials_from_role_credentials(
|
|
601
|
+
response: capo_sso.types.get_role_credentials_response.GetRoleCredentialsResponse,
|
|
602
|
+
) -> Credentials:
|
|
603
|
+
role = response.get("role_credentials") or {}
|
|
604
|
+
ak = role.get("access_key_id")
|
|
605
|
+
sk = role.get("secret_access_key")
|
|
606
|
+
if not ak or not sk:
|
|
607
|
+
raise SSOError("GetRoleCredentials response missing access key / secret key")
|
|
608
|
+
out: Credentials = {"access_key": ak, "secret_key": sk}
|
|
609
|
+
token = role.get("session_token")
|
|
610
|
+
if token:
|
|
611
|
+
out["session_token"] = token
|
|
612
|
+
expiration = role.get("expiration")
|
|
613
|
+
if expiration:
|
|
614
|
+
# SSO reports the expiry in epoch milliseconds
|
|
615
|
+
out["expiration"] = datetime.fromtimestamp(expiration / 1000, timezone.utc)
|
|
616
|
+
return out
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
@dataclass(frozen=True)
|
|
620
|
+
class AssumeRoleConfig:
|
|
621
|
+
role_arn: str
|
|
622
|
+
session_name: str
|
|
623
|
+
region: str
|
|
624
|
+
external_id: str | None
|
|
625
|
+
duration_seconds: int | None
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class AssumeRoleCredentialsProvider(CredentialsProvider):
|
|
629
|
+
"""Assume the profile's ``role_arn`` through STS ``AssumeRole``.
|
|
630
|
+
|
|
631
|
+
The credentials used to make the call come from the profile's
|
|
632
|
+
``source_profile`` (which may itself be an SSO profile, or another
|
|
633
|
+
``role_arn`` profile for role chaining) or from ``credential_source``.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
def __init__(
|
|
637
|
+
self,
|
|
638
|
+
client: Client | AsyncClient,
|
|
639
|
+
profile: str | None = None,
|
|
640
|
+
_visited: frozenset[str] = frozenset(),
|
|
641
|
+
) -> None:
|
|
642
|
+
self._client = client
|
|
643
|
+
self._profile = profile
|
|
644
|
+
self._visited = _visited
|
|
645
|
+
|
|
646
|
+
def resolve_identity(self) -> Credentials:
|
|
647
|
+
if isinstance(self._client, AsyncClient):
|
|
648
|
+
raise TypeError(
|
|
649
|
+
"AssumeRoleCredentialsProvider configured with AsyncClient; use aresolve_identity"
|
|
650
|
+
)
|
|
651
|
+
# resolve the config first so a profile without role_arn just skips
|
|
652
|
+
section, _ = _load_profile(self._profile)
|
|
653
|
+
config = assume_role_config(section)
|
|
654
|
+
require_dependency(capo_sts, "capo-sts")
|
|
655
|
+
source = self._source_provider(section)
|
|
656
|
+
sts = capo_sts.STSClient(
|
|
657
|
+
http_handler=self._client.handler,
|
|
658
|
+
region=config.region,
|
|
659
|
+
credentials_provider=cast("capo_sts.CredentialsProvider", source),
|
|
660
|
+
)
|
|
661
|
+
response = sts.assume_role(
|
|
662
|
+
config.role_arn,
|
|
663
|
+
config.session_name,
|
|
664
|
+
external_id=config.external_id,
|
|
665
|
+
duration_seconds=config.duration_seconds,
|
|
666
|
+
)
|
|
667
|
+
return credentials_from_sts(response.get("credentials"))
|
|
668
|
+
|
|
669
|
+
async def aresolve_identity(self) -> Credentials:
|
|
670
|
+
if not isinstance(self._client, AsyncClient):
|
|
671
|
+
raise TypeError(
|
|
672
|
+
"AssumeRoleCredentialsProvider configured with sync Client; use resolve_identity"
|
|
673
|
+
)
|
|
674
|
+
# resolve the config first so a profile without role_arn just skips
|
|
675
|
+
section, _ = _load_profile(self._profile)
|
|
676
|
+
config = assume_role_config(section)
|
|
677
|
+
require_dependency(capo_sts, "capo-sts")
|
|
678
|
+
source = self._source_provider(section)
|
|
679
|
+
sts = capo_sts.AsyncSTSClient(
|
|
680
|
+
http_handler=self._client.handler,
|
|
681
|
+
region=config.region,
|
|
682
|
+
credentials_provider=cast("capo_sts.CredentialsProvider", source),
|
|
683
|
+
)
|
|
684
|
+
response = await sts.assume_role(
|
|
685
|
+
config.role_arn,
|
|
686
|
+
config.session_name,
|
|
687
|
+
external_id=config.external_id,
|
|
688
|
+
duration_seconds=config.duration_seconds,
|
|
689
|
+
)
|
|
690
|
+
return credentials_from_sts(response.get("credentials"))
|
|
691
|
+
|
|
692
|
+
def _source_provider(
|
|
693
|
+
self, section: dict[str, str]
|
|
694
|
+
) -> IdentityProvider[Credentials]:
|
|
695
|
+
source_profile = section.get("source_profile")
|
|
696
|
+
credential_source = section.get("credential_source")
|
|
697
|
+
if source_profile and credential_source:
|
|
698
|
+
raise AssumeRoleError(
|
|
699
|
+
"source_profile and credential_source are mutually exclusive"
|
|
700
|
+
)
|
|
701
|
+
if credential_source:
|
|
702
|
+
match credential_source:
|
|
703
|
+
case "Environment":
|
|
704
|
+
return EnvCredentialsProvider()
|
|
705
|
+
case "Ec2InstanceMetadata":
|
|
706
|
+
return Ec2InstanceMetadataProvider(self._client)
|
|
707
|
+
case "EcsContainer":
|
|
708
|
+
return EcsContainerCredentialsProvider(self._client)
|
|
709
|
+
case _:
|
|
710
|
+
raise AssumeRoleError(
|
|
711
|
+
f"unsupported credential_source {credential_source!r}"
|
|
712
|
+
)
|
|
713
|
+
if source_profile in self._visited:
|
|
714
|
+
raise AssumeRoleError(
|
|
715
|
+
f"circular source_profile reference through {source_profile!r}"
|
|
716
|
+
)
|
|
717
|
+
assert source_profile is not None # assume_role_config rejects the empty case
|
|
718
|
+
return ChainedProvider(
|
|
719
|
+
AssumeRoleCredentialsProvider(
|
|
720
|
+
self._client, source_profile, self._visited | {source_profile}
|
|
721
|
+
),
|
|
722
|
+
SsoCredentialsProvider(self._client, source_profile),
|
|
723
|
+
ProfileCredentialsProvider(profile=source_profile),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class WebIdentityCredentialsProvider(CredentialsProvider):
|
|
728
|
+
"""Exchange an OIDC token for credentials via ``AssumeRoleWithWebIdentity``.
|
|
729
|
+
|
|
730
|
+
Reads ``AWS_WEB_IDENTITY_TOKEN_FILE``/``AWS_ROLE_ARN`` or the profile's
|
|
731
|
+
``web_identity_token_file``/``role_arn`` — this is how EKS service accounts
|
|
732
|
+
(IRSA) obtain credentials.
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
def __init__(
|
|
736
|
+
self, client: Client | AsyncClient, profile: str | None = None
|
|
737
|
+
) -> None:
|
|
738
|
+
self._client = client
|
|
739
|
+
self._profile = profile
|
|
740
|
+
|
|
741
|
+
def resolve_identity(self) -> Credentials:
|
|
742
|
+
if isinstance(self._client, AsyncClient):
|
|
743
|
+
raise TypeError(
|
|
744
|
+
"WebIdentityCredentialsProvider configured with AsyncClient; use aresolve_identity"
|
|
745
|
+
)
|
|
746
|
+
config, token = self._config()
|
|
747
|
+
require_dependency(capo_sts, "capo-sts")
|
|
748
|
+
sts = capo_sts.STSClient(
|
|
749
|
+
http_handler=self._client.handler, region=config.region
|
|
750
|
+
)
|
|
751
|
+
response = sts.assume_role_with_web_identity(
|
|
752
|
+
config.role_arn,
|
|
753
|
+
config.session_name,
|
|
754
|
+
token,
|
|
755
|
+
duration_seconds=config.duration_seconds,
|
|
756
|
+
config_overrides={"credentials_provider": None},
|
|
757
|
+
)
|
|
758
|
+
return credentials_from_sts(response.get("credentials"))
|
|
759
|
+
|
|
760
|
+
async def aresolve_identity(self) -> Credentials:
|
|
761
|
+
if not isinstance(self._client, AsyncClient):
|
|
762
|
+
raise TypeError(
|
|
763
|
+
"WebIdentityCredentialsProvider configured with sync Client; use resolve_identity"
|
|
764
|
+
)
|
|
765
|
+
config, token = self._config()
|
|
766
|
+
require_dependency(capo_sts, "capo-sts")
|
|
767
|
+
sts = capo_sts.AsyncSTSClient(
|
|
768
|
+
http_handler=self._client.handler, region=config.region
|
|
769
|
+
)
|
|
770
|
+
response = await sts.assume_role_with_web_identity(
|
|
771
|
+
config.role_arn,
|
|
772
|
+
config.session_name,
|
|
773
|
+
token,
|
|
774
|
+
duration_seconds=config.duration_seconds,
|
|
775
|
+
config_overrides={"credentials_provider": None},
|
|
776
|
+
)
|
|
777
|
+
return credentials_from_sts(response.get("credentials"))
|
|
778
|
+
|
|
779
|
+
def _config(self) -> tuple[AssumeRoleConfig, str]:
|
|
780
|
+
section, _ = _load_profile(self._profile)
|
|
781
|
+
token_file = os.environ.get("AWS_WEB_IDENTITY_TOKEN_FILE")
|
|
782
|
+
role_arn = os.environ.get("AWS_ROLE_ARN")
|
|
783
|
+
session_name = os.environ.get("AWS_ROLE_SESSION_NAME")
|
|
784
|
+
if not token_file or not role_arn:
|
|
785
|
+
# the env pair is all-or-nothing; otherwise fall back to the profile
|
|
786
|
+
token_file = section.get("web_identity_token_file")
|
|
787
|
+
role_arn = section.get("role_arn")
|
|
788
|
+
session_name = section.get("role_session_name")
|
|
789
|
+
if not token_file or not role_arn:
|
|
790
|
+
raise IdentityNotFound("no web identity token file / role arn configured")
|
|
791
|
+
path = Path(token_file).expanduser()
|
|
792
|
+
if not path.is_file():
|
|
793
|
+
raise AssumeRoleError(f"web identity token file {path} does not exist")
|
|
794
|
+
config = AssumeRoleConfig(
|
|
795
|
+
role_arn=role_arn,
|
|
796
|
+
session_name=session_name or default_session_name(),
|
|
797
|
+
region=sts_region(section),
|
|
798
|
+
external_id=None,
|
|
799
|
+
duration_seconds=int_or_none(section.get("duration_seconds")),
|
|
800
|
+
)
|
|
801
|
+
return config, path.read_text().strip()
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def assume_role_config(section: dict[str, str]) -> AssumeRoleConfig:
|
|
805
|
+
role_arn = section.get("role_arn")
|
|
806
|
+
if not role_arn:
|
|
807
|
+
raise IdentityNotFound("profile has no role_arn")
|
|
808
|
+
if section.get("web_identity_token_file"):
|
|
809
|
+
# handled by WebIdentityCredentialsProvider, not AssumeRole
|
|
810
|
+
raise IdentityNotFound("profile uses web_identity_token_file")
|
|
811
|
+
if not section.get("source_profile") and not section.get("credential_source"):
|
|
812
|
+
raise IdentityNotFound(
|
|
813
|
+
"role_arn profile has no source_profile/credential_source"
|
|
814
|
+
)
|
|
815
|
+
if section.get("mfa_serial"):
|
|
816
|
+
raise AssumeRoleError("mfa_serial profiles are not supported")
|
|
817
|
+
return AssumeRoleConfig(
|
|
818
|
+
role_arn=role_arn,
|
|
819
|
+
session_name=section.get("role_session_name") or default_session_name(),
|
|
820
|
+
region=sts_region(section),
|
|
821
|
+
external_id=section.get("external_id"),
|
|
822
|
+
duration_seconds=int_or_none(section.get("duration_seconds")),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def default_session_name() -> str:
|
|
827
|
+
return f"capo-session-{int(time.time())}"
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def sts_region(section: dict[str, str]) -> str:
|
|
831
|
+
return os.environ.get("AWS_REGION") or section.get("region") or "us-east-1"
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def int_or_none(value: str | None) -> int | None:
|
|
835
|
+
return int(value) if value else None
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def credentials_from_sts(
|
|
839
|
+
credentials: capo_sts.types.credentials.Credentials | None,
|
|
840
|
+
) -> Credentials:
|
|
841
|
+
if credentials is None:
|
|
842
|
+
raise AssumeRoleError("STS response contained no credentials")
|
|
843
|
+
return {
|
|
844
|
+
"access_key": credentials["access_key_id"],
|
|
845
|
+
"secret_key": credentials["secret_access_key"],
|
|
846
|
+
"session_token": credentials["session_token"],
|
|
847
|
+
"expiration": credentials["expiration"],
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _parse_iso8601(value: str) -> datetime:
|
|
852
|
+
# tolerate trailing 'Z'
|
|
853
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _credentials_from_json(data: dict[str, object]) -> Credentials:
|
|
857
|
+
ak = data.get("AccessKeyId")
|
|
858
|
+
sk = data.get("SecretAccessKey")
|
|
859
|
+
if not isinstance(ak, str) or not isinstance(sk, str):
|
|
860
|
+
raise IdentityNotFound(
|
|
861
|
+
"credentials response missing AccessKeyId/SecretAccessKey"
|
|
862
|
+
)
|
|
863
|
+
out: Credentials = {"access_key": ak, "secret_key": sk}
|
|
864
|
+
token = data.get("Token")
|
|
865
|
+
if isinstance(token, str):
|
|
866
|
+
out["session_token"] = token
|
|
867
|
+
exp = data.get("Expiration")
|
|
868
|
+
if isinstance(exp, str):
|
|
869
|
+
out["expiration"] = _parse_iso8601(exp)
|
|
870
|
+
return out
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def default_aws_credentials_chain(
|
|
874
|
+
client: Client | AsyncClient,
|
|
875
|
+
) -> IdentityProvider[Credentials]:
|
|
876
|
+
return CachedProvider(
|
|
877
|
+
ChainedProvider(
|
|
878
|
+
EnvCredentialsProvider(),
|
|
879
|
+
AssumeRoleCredentialsProvider(client),
|
|
880
|
+
WebIdentityCredentialsProvider(client),
|
|
881
|
+
SsoCredentialsProvider(client),
|
|
882
|
+
ProfileCredentialsProvider(),
|
|
883
|
+
EcsContainerCredentialsProvider(client),
|
|
884
|
+
Ec2InstanceMetadataProvider(client),
|
|
885
|
+
)
|
|
886
|
+
)
|