clear-skies-aws 1.10.2__py3-none-any.whl → 2.0.2__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.
Files changed (86) hide show
  1. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/METADATA +36 -35
  2. clear_skies_aws-2.0.2.dist-info/RECORD +63 -0
  3. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/WHEEL +1 -1
  4. clear_skies_aws-2.0.2.dist-info/licenses/LICENSE +21 -0
  5. clearskies_aws/__init__.py +15 -2
  6. clearskies_aws/actions/__init__.py +13 -106
  7. clearskies_aws/actions/action_aws.py +74 -57
  8. clearskies_aws/actions/assume_role.py +43 -30
  9. clearskies_aws/actions/ses.py +82 -73
  10. clearskies_aws/actions/sns.py +27 -30
  11. clearskies_aws/actions/sqs.py +32 -33
  12. clearskies_aws/actions/step_function.py +38 -31
  13. clearskies_aws/backends/__init__.py +11 -4
  14. clearskies_aws/backends/backend.py +106 -0
  15. clearskies_aws/backends/dynamo_db_backend.py +150 -155
  16. clearskies_aws/backends/dynamo_db_condition_parser.py +40 -80
  17. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +179 -337
  18. clearskies_aws/backends/sqs_backend.py +32 -51
  19. clearskies_aws/configs/__init__.py +0 -0
  20. clearskies_aws/contexts/__init__.py +23 -10
  21. clearskies_aws/contexts/cli_web_socket_mock.py +19 -0
  22. clearskies_aws/contexts/lambda_alb.py +76 -0
  23. clearskies_aws/contexts/lambda_api_gateway.py +75 -28
  24. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +56 -29
  25. clearskies_aws/contexts/lambda_invocation.py +15 -44
  26. clearskies_aws/contexts/lambda_sns.py +8 -33
  27. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +14 -36
  28. clearskies_aws/di/__init__.py +6 -1
  29. clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
  30. clearskies_aws/di/inject/__init__.py +6 -0
  31. clearskies_aws/di/inject/boto3.py +15 -0
  32. clearskies_aws/di/inject/boto3_session.py +13 -0
  33. clearskies_aws/di/inject/parameter_store.py +15 -0
  34. clearskies_aws/{handlers → endpoints}/secrets_manager_rotation.py +76 -55
  35. clearskies_aws/endpoints/simple_body_routing.py +41 -0
  36. clearskies_aws/input_outputs/__init__.py +21 -8
  37. clearskies_aws/input_outputs/{cli_websocket_mock.py → cli_web_socket_mock.py} +9 -3
  38. clearskies_aws/input_outputs/lambda_alb.py +53 -0
  39. clearskies_aws/input_outputs/lambda_api_gateway.py +106 -88
  40. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +69 -6
  41. clearskies_aws/input_outputs/lambda_input_output.py +87 -0
  42. clearskies_aws/input_outputs/lambda_invocation.py +77 -26
  43. clearskies_aws/input_outputs/lambda_sns.py +66 -39
  44. clearskies_aws/input_outputs/lambda_sqs_standard.py +70 -40
  45. clearskies_aws/mocks/actions/ses.py +25 -19
  46. clearskies_aws/mocks/actions/sns.py +18 -12
  47. clearskies_aws/mocks/actions/sqs.py +18 -12
  48. clearskies_aws/mocks/actions/step_function.py +19 -13
  49. clearskies_aws/models/__init__.py +0 -0
  50. clearskies_aws/models/web_socket_connection_model.py +182 -0
  51. clearskies_aws/secrets/__init__.py +13 -7
  52. clearskies_aws/secrets/additional_configs/__init__.py +10 -2
  53. clearskies_aws/secrets/additional_configs/iam_db_auth.py +26 -16
  54. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +43 -39
  55. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +30 -31
  56. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +70 -49
  57. clearskies_aws/secrets/akeyless_with_ssm_cache.py +32 -18
  58. clearskies_aws/secrets/parameter_store.py +34 -32
  59. clearskies_aws/secrets/secrets.py +16 -0
  60. clearskies_aws/secrets/secrets_manager.py +78 -57
  61. clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
  62. clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
  63. clearskies_aws/actions/assume_role_test.py +0 -72
  64. clearskies_aws/actions/ses_test.py +0 -89
  65. clearskies_aws/actions/sns_test.py +0 -77
  66. clearskies_aws/actions/sqs_test.py +0 -127
  67. clearskies_aws/actions/step_function_test.py +0 -103
  68. clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
  69. clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
  70. clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
  71. clearskies_aws/backends/sqs_backend_test.py +0 -31
  72. clearskies_aws/contexts/cli.py +0 -19
  73. clearskies_aws/contexts/cli_websocket_mock.py +0 -33
  74. clearskies_aws/contexts/lambda_elb.py +0 -30
  75. clearskies_aws/contexts/lambda_http_gateway.py +0 -30
  76. clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
  77. clearskies_aws/contexts/wsgi.py +0 -19
  78. clearskies_aws/di/standard_dependencies.py +0 -60
  79. clearskies_aws/handlers/simple_body_routing.py +0 -39
  80. clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
  81. clearskies_aws/input_outputs/lambda_elb.py +0 -21
  82. clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
  83. clearskies_aws/secrets/parameter_store_test.py +0 -18
  84. clearskies_aws/secrets/secrets_manager_test.py +0 -18
  85. clearskies_aws/web_socket_connection_model.py +0 -43
  86. clearskies_aws/{handlers → endpoints}/__init__.py +1 -1
@@ -1,26 +1,32 @@
1
- from types import ModuleType
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
2
5
  from clearskies import Model
3
- from ...actions.step_function import StepFunction as BaseStepFunction
4
- class StepFunction(BaseStepFunction):
5
- calls = None
6
+ from types_boto3_stepfunctions import SFNClient
6
7
 
7
- def __init__(self, environment, boto3, di):
8
- super().__init__(environment, boto3, di)
8
+ from clearskies_aws.actions.step_function import StepFunction as BaseStepFunction
9
+
10
+
11
+ class StepFunction(BaseStepFunction):
12
+ calls: list[dict[str, Any]] | None = None
9
13
 
10
14
  @classmethod
11
15
  def mock(cls, di):
12
- StepFunction.calls = []
16
+ cls.calls = []
13
17
  di.mock_class(BaseStepFunction, StepFunction)
14
18
 
15
- def _execute_action(self, client: ModuleType, model: Model) -> None:
19
+ def _execute_action(self, client: SFNClient, model: Model) -> None:
16
20
  """Send a notification as configured."""
17
- if StepFunction.calls == None:
21
+ if StepFunction.calls is None:
18
22
  StepFunction.calls = []
19
23
 
20
- StepFunction.calls.append({
21
- "stateMachineArn": self.get_arn(model),
22
- "input": self.get_message_body(model),
23
- })
24
+ StepFunction.calls.append(
25
+ {
26
+ "stateMachineArn": self.get_arn(model),
27
+ "input": self.get_message_body(model),
28
+ }
29
+ )
24
30
 
25
31
  if self.column_to_store_execution_arn:
26
32
  model.save({self.column_to_store_execution_arn: "mock_execution_arn"})
File without changes
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import clearskies
6
+
7
+ import clearskies_aws
8
+
9
+
10
+ class WebSocketConnectionModel(clearskies.Model):
11
+ """
12
+ Help manage message sending to websocket connections.
13
+
14
+ ## Working with Websockets
15
+
16
+ This is a partial model class to help send messages to websocket connections in an API gateway.
17
+ With a API Gateway managed websocket, the API gateway assigns an id to every connection, and you
18
+ send messages to the API gateway itself flagged for some client, via its connection id. It
19
+ helps to understand that you don't need to be connected to the websocket itself to send messages
20
+ to the things connected to it. Instead, you just need the necessary permission on the API gateway
21
+ and you need to know the connection id of the client you want to send a message to. For reference,
22
+ the necessary AWS permission is:
23
+
24
+ ```
25
+ {
26
+ "Version": "2012-10-17",
27
+ "Statement": [
28
+ {
29
+ "Effect": "Allow",
30
+ "Action": ["execute-api:ManageConnections"],
31
+ "Resource": "arn:aws:execute-api:${aws_region_name}:${aws_account_id}:${api_gateway_id}/{stage}/*",
32
+ }
33
+ ],
34
+ }
35
+ ```
36
+
37
+ A simple flow that reproduces a pub/sub approach is:
38
+
39
+ 1. Client connects to the API gateway via a websocket, and the backend records the connection id
40
+ in a backend somewhere
41
+ 2. Client sends a message through the websocket to "register"/"subscribe" for some resource, and
42
+ the backend service updates the record for the connection to record what resource it is
43
+ subscribed to
44
+ 3. If a server needs to send a message to everyone subscribed to a resource, it queries the backends
45
+ for all records connected to the resource in question and sends a message to their connection ids
46
+ through the backend.
47
+ 4. If a client needs to send a message to everyone subscribed to a resource, it sends a message
48
+ through the websocket to some backend service which then passes the message along to the appropriate
49
+ connections, just as in #3 above.
50
+
51
+ Most examples with Websockets and API Gateway use dynamodb for storage. You can, of course, use whatever
52
+ backend you want. Still, below is an example pub/sub application to demonstrate how to build a basic
53
+ websocket app:
54
+
55
+ ```
56
+ import clearskies
57
+ import clearskies_aws
58
+
59
+ #####################
60
+ ## Our model class ##
61
+ #####################
62
+
63
+ class Client(clearskies_aws.models.WebSocketConnectionModel):
64
+ backend = clearskies_aws.backends.DynamoDbBackend()
65
+
66
+ # the base WebSocketConnectionModel class defines a string
67
+ # column called `connection_id` and sets it as the id column,
68
+ # so those are already set. We just need any additional columns
69
+ resource_id = clearskies.columns.String()
70
+
71
+ ###########################
72
+ ## Our application logic ##
73
+ ###########################
74
+
75
+ def on_connect(clients, connection_id):
76
+ clients.create({"connection_id": connection_id})
77
+
78
+ def on_subscribe(clients, connection_id, request_data):
79
+ client = clients.find(f"connection_id={connection_id}")
80
+ if client.exists:
81
+ # we blindly save the request id, which makes it user-generated. This also
82
+ # allows the client to "unsubscribe" by sending up a blank resource
83
+ client.save({"resource_id": request_data["resource_id"])
84
+
85
+ def on_publish(clients, connection_id, request_data):
86
+ my_client = clients.find(f"connection_id={connection_id}")
87
+ message = request_data.get("message")
88
+
89
+ # The problem with our standard input validation is that we can't return a response
90
+ # to the client with an error message. This is not a transactional system. Instead,
91
+ # we would have to send the client a new message with the error in it, which is not
92
+ # something the clearskies endpoints are designed for.
93
+ if not message or not my_client.resource_id:
94
+ return
95
+
96
+ for client in clients.where(f"resource_id={my_client.resource_id}").paginate_all():
97
+ if client.connection_id == my_client.connection_id:
98
+ continue
99
+
100
+ # the send function is provided by clearskies_aws.models.WebSocketConnectionModel
101
+ client.send({"message": message})
102
+
103
+ def on_disconnect(clients, connection_id):
104
+ clients.find(f"connection_id={connection_id}").delete(except_if_not_exists=False)
105
+
106
+ ######################################
107
+ ## Wiring it all up with clearskies ##
108
+ ######################################
109
+
110
+ # We're going to build one application, even though each action gets it's own lambda.
111
+ # The URLs aren't used for routing, but simply to allow us to select which function
112
+ # is associated with each lambda. Actual routing still happens in the API Gateway
113
+ websocket_application = clearskies_aws.contexts.LambdaApiGatewayWebSocket(
114
+ clearskies.EndpointGroup([
115
+ clearskies.endpoints.Callable(
116
+ on_connect,
117
+ url="on_connect",
118
+ ),
119
+ clearskies.endpoints.Callable(
120
+ on_subscribe,
121
+ url="on_subscribe",
122
+ ),
123
+ clearskies.endpoints.Callable(
124
+ on_publish,
125
+ url="on_publish",
126
+ ),
127
+ clearskies.endpoints.Callable(
128
+ on_disconnect,
129
+ url="on_disconnect",
130
+ ),
131
+ ]),
132
+ classes=[Client],
133
+ )
134
+
135
+ ################################
136
+ ## The actual lambda handlers ##
137
+ ################################
138
+
139
+ def on_connect_handler(event, context):
140
+ return websocket_application(url="on_connect")
141
+
142
+ def on_subscribe_handler(event, context):
143
+ return websocket_application(url="on_subscribe")
144
+
145
+ def on_publish_handler(event, context):
146
+ return websocket_application(url="on_publish")
147
+
148
+ def on_disconnect_handler(event, context):
149
+ return websocket_application(url="on_disconnect")
150
+ ```
151
+
152
+ """
153
+
154
+ id_column_name = "connection_id"
155
+
156
+ boto3 = clearskies_aws.di.inject.Boto3()
157
+ connection_id = clearskies.columns.String()
158
+ input_output = clearskies.di.inject.InputOutput()
159
+
160
+ def send(self, message):
161
+ if not self:
162
+ raise ValueError("Cannot send message to non-existent connection.")
163
+ if not self.connection_id:
164
+ raise ValueError(
165
+ f"Hmmm... I couldn't find the connection id for the {self.__class__.__name__}. I'm picky about id column names. Can you please make sure I have a column called connection_id and that it contains the connection id?"
166
+ )
167
+
168
+ domain = self.input_output.context_specifics()["domain"]
169
+ stage = self.input_output.context_specifics()["stage"]
170
+ # only include the stage if we're using the default AWS domain - not with a custom domain
171
+ if ".amazonaws.com" in domain:
172
+ endpoint_url = f"https://{domain}/{stage}"
173
+ else:
174
+ endpoint_url = f"https://{domain}"
175
+ api_gateway = self.boto3.client("apigatewaymanagementapi", endpoint_url=endpoint_url)
176
+
177
+ bytes_message = json.dumps(message).encode("utf-8")
178
+ try:
179
+ response = api_gateway.post_to_connection(Data=bytes_message, ConnectionId=self.connection_id)
180
+ except api_gateway.exceptions.GoneException:
181
+ self.delete()
182
+ return response
@@ -1,7 +1,13 @@
1
- from clearskies import BindingConfig
2
- from .parameter_store import ParameterStore
3
- from .secrets_manager import SecretsManager
4
- from .akeyless_with_ssm_cache import AkeylessWithSsmCache
5
- from . import additional_configs
6
- def akeyless_with_ssm_cache(*args, **kwargs):
7
- return BindingConfig(AkeylessWithSsmCache, *args, **kwargs)
1
+ from clearskies_aws.secrets import additional_configs
2
+ from clearskies_aws.secrets.akeyless_with_ssm_cache import AkeylessWithSsmCache
3
+ from clearskies_aws.secrets.parameter_store import ParameterStore
4
+ from clearskies_aws.secrets.secrets import Secrets
5
+ from clearskies_aws.secrets.secrets_manager import SecretsManager
6
+
7
+ __all__ = [
8
+ "Secrets",
9
+ "ParameterStore",
10
+ "SecretsManager",
11
+ "AkeylessWithSsmCache",
12
+ "additional_configs",
13
+ ]
@@ -1,7 +1,9 @@
1
- from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import MySQLConnectionDynamicProducerViaSSHCertBastion
2
- from .mysql_connection_dynamic_producer_via_ssm_bastion import MySQLConnectionDynamicProducerViaSSMBastion
3
1
  from .iam_db_auth import IAMDBAuth
4
2
  from .iam_db_auth_with_ssm import IAMDBAuthWithSSM
3
+ from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import MySQLConnectionDynamicProducerViaSSHCertBastion
4
+ from .mysql_connection_dynamic_producer_via_ssm_bastion import MySQLConnectionDynamicProducerViaSSMBastion
5
+
6
+
5
7
  def mysql_connection_dynamic_producer_via_ssh_cert_bastion(
6
8
  producer_name=None,
7
9
  bastion_host=None,
@@ -26,6 +28,8 @@ def mysql_connection_dynamic_producer_via_ssh_cert_bastion(
26
28
  database_name=database_name,
27
29
  local_proxy_port=local_proxy_port,
28
30
  )
31
+
32
+
29
33
  def mysql_connection_dynamic_producer_via_ssm_bastion(
30
34
  producer_name=None,
31
35
  bastion_instance_id=None,
@@ -48,7 +52,11 @@ def mysql_connection_dynamic_producer_via_ssm_bastion(
48
52
  database_name=database_name,
49
53
  local_proxy_port=local_proxy_port,
50
54
  )
55
+
56
+
51
57
  def iam_db_auth():
52
58
  return IAMDBAuth()
59
+
60
+
53
61
  def iam_db_auth_with_ssm():
54
62
  return IAMDBAuthWithSSM()
@@ -1,29 +1,39 @@
1
- import clearskies
1
+ from __future__ import annotations
2
+
2
3
  import os
4
+
5
+ import clearskies
6
+
7
+
3
8
  class IAMDBAuth(clearskies.di.AdditionalConfig):
4
9
  def provide_boto3(self):
5
10
  import boto3
11
+
6
12
  return boto3
7
13
 
8
14
  def provide_connection_details(self, environment, boto3):
9
15
  """
10
- I really need to make these configurable - both the values themselves and the environment
11
- variables that things get pulled from.
16
+ Make configuration values and environment variables customizable.
17
+
18
+ Allows both the values and the environment variable names to be set for flexible configuration.
19
+
20
+ Returns:
21
+ dict: Connection details for IAM DB authentication.
12
22
  """
13
- endpoint = environment.get('db_endpoint')
14
- username = environment.get('db_username')
15
- database = environment.get('db_database')
16
- region = environment.get('AWS_REGION')
17
- ssl_ca_bundle_name = environment.get('ssl_ca_bundle_filename')
18
- os.environ['LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN'] = '1'
23
+ endpoint = environment.get("db_endpoint")
24
+ username = environment.get("db_username")
25
+ database = environment.get("db_database")
26
+ region = environment.get("AWS_REGION")
27
+ ssl_ca_bundle_name = environment.get("ssl_ca_bundle_filename")
28
+ os.environ["LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN"] = "1"
19
29
 
20
- rds_api = boto3.Session().client('rds')
21
- rds_token = rds_api.generate_db_auth_token(DBHostname=endpoint, Port='3306', DBUsername=username, Region=region)
30
+ rds_api = boto3.Session().client("rds")
31
+ rds_token = rds_api.generate_db_auth_token(DBHostname=endpoint, Port="3306", DBUsername=username, Region=region)
22
32
 
23
33
  return {
24
- 'username': username,
25
- 'password': rds_token,
26
- 'host': endpoint,
27
- 'database': database,
28
- 'ssl_ca': ssl_ca_bundle_name,
34
+ "username": username,
35
+ "password": rds_token,
36
+ "host": endpoint,
37
+ "database": database,
38
+ "ssl_ca": ssl_ca_bundle_name,
29
39
  }
@@ -1,57 +1,61 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
4
+
2
5
  import clearskies
6
+
7
+
3
8
  class IAMDBAuthWithSSM(clearskies.di.AdditionalConfig):
4
9
  def provide_subprocess(self):
5
10
  import subprocess
11
+
6
12
  return subprocess
7
13
 
8
14
  def provide_socket(self):
9
15
  import socket
16
+
10
17
  return socket
11
18
 
12
19
  def provide_connection_details(self, environment, subprocess, socket, boto3):
13
20
  local_port = self.open_tunnel(environment, subprocess, socket, boto3)
14
21
 
15
22
  return {
16
- 'host': '127.0.0.1',
17
- 'database': environment.get('db_database'),
18
- 'username': environment.get('db_username'),
19
- 'password': self.get_password(environment, boto3),
20
- 'ssl_ca': 'rds-cert-bundle.pem',
21
- 'port': local_port,
23
+ "host": "127.0.0.1",
24
+ "database": environment.get("db_database"),
25
+ "username": environment.get("db_username"),
26
+ "password": self.get_password(environment, boto3),
27
+ "ssl_ca": "rds-cert-bundle.pem",
28
+ "port": local_port,
22
29
  }
23
30
 
24
31
  def get_password(self, environment, boto3):
25
- endpoint = environment.get('db_endpoint')
26
- username = environment.get('db_username')
27
- region = environment.get('db_region')
32
+ endpoint = environment.get("db_endpoint")
33
+ username = environment.get("db_username")
34
+ region = environment.get("db_region")
28
35
 
29
- rds_api = boto3.Session().client('rds', region_name=region)
30
- return rds_api.generate_db_auth_token(DBHostname=endpoint, Port='3306', DBUsername=username, Region=region)
36
+ rds_api = boto3.Session().client("rds", region_name=region)
37
+ return rds_api.generate_db_auth_token(DBHostname=endpoint, Port="3306", DBUsername=username, Region=region)
31
38
 
32
39
  def open_tunnel(self, environment, subprocess, socket, boto3):
33
- endpoint = environment.get('db_endpoint')
34
- region = environment.get('db_region')
35
- instance_name = environment.get('instance_name')
36
- local_proxy_port = int(environment.get('local_proxy_port', '9000'))
40
+ endpoint = environment.get("db_endpoint")
41
+ region = environment.get("db_region")
42
+ instance_name = environment.get("instance_name")
43
+ local_proxy_port = int(environment.get("local_proxy_port", "9000"))
37
44
 
38
- ec2_api = boto3.client('ec2', region_name=region)
45
+ ec2_api = boto3.client("ec2", region_name=region)
39
46
  running_instances = ec2_api.describe_instances(
40
- Filters=[{
41
- 'Name': 'tag:Name',
42
- 'Values': [instance_name]
43
- }, {
44
- 'Name': 'instance-state-name',
45
- 'Values': ['running']
46
- }],
47
+ Filters=[
48
+ {"Name": "tag:Name", "Values": [instance_name]},
49
+ {"Name": "instance-state-name", "Values": ["running"]},
50
+ ],
47
51
  )
48
52
  instance_ids = []
49
- for reservation in running_instances['Reservations']:
50
- for instance in reservation['Instances']:
51
- instance_ids.append(instance['InstanceId'])
53
+ for reservation in running_instances["Reservations"]:
54
+ for instance in reservation["Instances"]:
55
+ instance_ids.append(instance["InstanceId"])
52
56
 
53
57
  if len(instance_ids) == 0:
54
- raise ValueError('Failed to launch SSM tunnel! Cannot find bastion!')
58
+ raise ValueError("Failed to launch SSM tunnel! Cannot find bastion!")
55
59
 
56
60
  instance_id = instance_ids.pop()
57
61
  self._connect_to_bastion(local_proxy_port, instance_id, endpoint, subprocess, socket)
@@ -59,21 +63,21 @@ class IAMDBAuthWithSSM(clearskies.di.AdditionalConfig):
59
63
 
60
64
  def _connect_to_bastion(self, local_proxy_port, instance_id, endpoint, subprocess, socket):
61
65
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
62
- result = sock.connect_ex(('127.0.0.1', local_proxy_port))
66
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
63
67
  if result == 0:
64
68
  sock.close()
65
69
  return
66
70
 
67
71
  tunnel_command = [
68
- 'aws',
69
- '--region',
70
- 'us-east-1',
71
- 'ssm',
72
- 'start-session',
73
- '--target',
74
- '{}'.format(instance_id),
75
- '--document-name',
76
- 'AWS-StartPortForwardingSessionToRemoteHost',
72
+ "aws",
73
+ "--region",
74
+ "us-east-1",
75
+ "ssm",
76
+ "start-session",
77
+ "--target",
78
+ "{}".format(instance_id),
79
+ "--document-name",
80
+ "AWS-StartPortForwardingSessionToRemoteHost",
77
81
  '--parameters={{"host":["{}"], "portNumber":["3306"],"localPortNumber":["{}"]}}'.format(
78
82
  endpoint, local_proxy_port
79
83
  ),
@@ -86,7 +90,7 @@ class IAMDBAuthWithSSM(clearskies.di.AdditionalConfig):
86
90
  attempts += 1
87
91
  time.sleep(0.5)
88
92
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
89
- result = sock.connect_ex(('127.0.0.1', local_proxy_port))
93
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
90
94
  if result == 0:
91
95
  return
92
- raise ValueError('Failed to launch SSM tunnel with command: ' + ' '.join(tunnel_command))
96
+ raise ValueError("Failed to launch SSM tunnel with command: " + " ".join(tunnel_command))
@@ -1,9 +1,14 @@
1
- from clearskies.secrets.additional_configs import MySQLConnectionDynamicProducerViaSSHCertBastion as Base
2
- from pathlib import Path
1
+ from __future__ import annotations
2
+
3
+ import os
3
4
  import socket
4
5
  import subprocess
5
- import os
6
6
  import time
7
+ from pathlib import Path
8
+
9
+ from clearskies.secrets.additional_configs import MySQLConnectionDynamicProducerViaSSHCertBastion as Base
10
+
11
+
7
12
  class MySQLConnectionDynamicProducerViaSSHCertBastion(Base):
8
13
  _config = None
9
14
  _boto3 = None
@@ -19,20 +24,20 @@ class MySQLConnectionDynamicProducerViaSSHCertBastion(Base):
19
24
  local_proxy_port=None,
20
25
  cert_issuer_name=None,
21
26
  database_host=None,
22
- database_name=None
27
+ database_name=None,
23
28
  ):
24
29
  # not using kwargs because I want the argument list to be explicit
25
30
  self.config = {
26
- 'producer_name': producer_name,
27
- 'bastion_host': bastion_host,
28
- 'bastion_region': bastion_region,
29
- 'bastion_name': bastion_name,
30
- 'bastion_username': bastion_username,
31
- 'public_key_file_path': public_key_file_path,
32
- 'local_proxy_port': local_proxy_port,
33
- 'cert_issuer_name': cert_issuer_name,
34
- 'database_host': database_host,
35
- 'database_name': database_name,
31
+ "producer_name": producer_name,
32
+ "bastion_host": bastion_host,
33
+ "bastion_region": bastion_region,
34
+ "bastion_name": bastion_name,
35
+ "bastion_username": bastion_username,
36
+ "public_key_file_path": public_key_file_path,
37
+ "local_proxy_port": local_proxy_port,
38
+ "cert_issuer_name": cert_issuer_name,
39
+ "database_host": database_host,
40
+ "database_name": database_name,
36
41
  }
37
42
 
38
43
  def provide_connection_details(self, environment, secrets, boto3):
@@ -40,42 +45,36 @@ class MySQLConnectionDynamicProducerViaSSHCertBastion(Base):
40
45
  return super().provide_connection_details(environment, secrets)
41
46
 
42
47
  def _get_bastion_host(self, environment):
43
- bastion_host = self._fetch_config(environment, 'bastion_host', 'akeyless_mysql_bastion_host', default='')
44
- bastion_name = self._fetch_config(environment, 'bastion_name', 'akeyless_mysql_bastion_name', default='')
48
+ bastion_host = self._fetch_config(environment, "bastion_host", "akeyless_mysql_bastion_host", default="")
49
+ bastion_name = self._fetch_config(environment, "bastion_name", "akeyless_mysql_bastion_name", default="")
45
50
  if bastion_host:
46
51
  return bastion_host
47
52
  if bastion_name:
48
- bastion_region = self._fetch_config(environment, 'bastion_region', 'akeyless_mysql_bastion_region')
53
+ bastion_region = self._fetch_config(environment, "bastion_region", "akeyless_mysql_bastion_region")
49
54
  return self._public_ip_from_name(bastion_name, bastion_region)
50
55
  raise ValueError(
51
56
  f"I was asked to connect to a database via an AKeyless dynamic producer through an SSH bastion with certificate auth, but I'm missing some configuration. I need either the bastion host or the name of the instance in AWS. These can be set in the call to `clearskies.backends.akeyless_aws.mysql_connection_dynamic_producer_via_ssh_cert_bastion()` by providing the 'bastion_host' or 'bastion_name' argument, or by setting an environment variable named 'akeyless_mysql_bastion_host' or 'akeyless_mysql_bastion_name'."
52
57
  )
53
58
 
54
59
  def _public_ip_from_name(self, bastion_name, bastion_region):
55
- ec2 = self._boto3.client('ec2', region_name=bastion_region)
60
+ ec2 = self._boto3.client("ec2", region_name=bastion_region)
56
61
  response = ec2.describe_instances(
57
62
  Filters=[
58
- {
59
- 'Name': 'tag:Name',
60
- 'Values': [bastion_name]
61
- },
62
- {
63
- 'Name': 'instance-state-name',
64
- 'Values': ['running']
65
- },
63
+ {"Name": "tag:Name", "Values": [bastion_name]},
64
+ {"Name": "instance-state-name", "Values": ["running"]},
66
65
  ],
67
66
  )
68
- if not response.get('Reservations'):
67
+ if not response.get("Reservations"):
69
68
  raise ValueError(
70
69
  f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
71
70
  )
72
- if not response.get('Reservations')[0].get('Instances'):
71
+ if not response.get("Reservations")[0].get("Instances"):
73
72
  raise ValueError(
74
73
  f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
75
74
  )
76
- instance = response.get('Reservations')[0].get('Instances')[0]
77
- if not instance.get('PublicIpAddress'):
75
+ instance = response.get("Reservations")[0].get("Instances")[0]
76
+ if not instance.get("PublicIpAddress"):
78
77
  raise ValueError(
79
78
  f"I found the bastion instance with a name of '{bastion_name}' in region '{bastion_region}', but it doesn't have a public IP address"
80
79
  )
81
- return instance.get('PublicIpAddress')
80
+ return instance.get("PublicIpAddress")