cloudx-proxy 0.3.14__tar.gz → 0.4.0__tar.gz

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 (22) hide show
  1. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/CHANGELOG.md +15 -0
  2. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/PKG-INFO +1 -1
  3. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/_version.py +2 -2
  4. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/cli.py +15 -3
  5. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/core.py +23 -5
  6. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/setup.py +334 -77
  7. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/PKG-INFO +1 -1
  8. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.github/workflows/release.yml +0 -0
  9. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.gitignore +0 -0
  10. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/.releaserc +0 -0
  11. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/CONTRIBUTING.md +0 -0
  12. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/LICENSE +0 -0
  13. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/README.md +0 -0
  14. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy/__init__.py +0 -0
  15. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
  16. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
  17. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/entry_points.txt +0 -0
  18. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/requires.txt +0 -0
  19. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/cloudx_proxy.egg-info/top_level.txt +0 -0
  20. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/package.json +0 -0
  21. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/pyproject.toml +0 -0
  22. {cloudx_proxy-0.3.14 → cloudx_proxy-0.4.0}/setup.cfg +0 -0
@@ -1,3 +1,18 @@
1
+ # [0.4.0](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.15...v0.4.0) (2025-03-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * added 1password integration ([4ac2340](https://github.com/easytocloud/cloudX-proxy/commit/4ac2340d6174ded129482e0fcd91a9cef0ab136d))
7
+
8
+ ## [0.3.15](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.14...v0.3.15) (2025-03-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * added --ssh-config ([893d919](https://github.com/easytocloud/cloudX-proxy/commit/893d919f7ef30dc5fd41a06b2c032d0035180e80))
14
+ * added --ssh-config also in connect ([75b7b3b](https://github.com/easytocloud/cloudX-proxy/commit/75b7b3b5ecac5f1a1012ce9d4905bc5aed444915))
15
+
1
16
  ## [0.3.14](https://github.com/easytocloud/cloudX-proxy/compare/v0.3.13...v0.3.14) (2025-03-06)
2
17
 
3
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cloudx-proxy
3
- Version: 0.3.14
3
+ Version: 0.4.0
4
4
  Summary: SSH proxy command to connect VSCode with Cloud9/CloudX instance using AWS Systems Manager
5
5
  Author-email: easytocloud <info@easytocloud.com>
6
6
  License: MIT License
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.14'
21
- __version_tuple__ = version_tuple = (0, 3, 14)
20
+ __version__ = version = '0.4.0'
21
+ __version_tuple__ = version_tuple = (0, 4, 0)
@@ -17,8 +17,9 @@ def cli():
17
17
  @click.option('--profile', default='vscode', help='AWS profile to use (default: vscode)')
18
18
  @click.option('--region', help='AWS region (default: from profile, or eu-west-1 if not set)')
19
19
  @click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
20
+ @click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
20
21
  @click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
21
- def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str, aws_env: str):
22
+ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str, ssh_config: str, aws_env: str):
22
23
  """Connect to an EC2 instance via SSM.
23
24
 
24
25
  INSTANCE_ID is the EC2 instance ID to connect to (e.g., i-0123456789abcdef0)
@@ -35,6 +36,7 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
35
36
  profile=profile,
36
37
  region=region,
37
38
  ssh_key=ssh_key,
39
+ ssh_config=ssh_config,
38
40
  aws_env=aws_env
39
41
  )
40
42
 
@@ -48,8 +50,10 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
48
50
  @cli.command()
49
51
  @click.option('--profile', default='vscode', help='AWS profile to use (default: vscode)')
50
52
  @click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
53
+ @click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
51
54
  @click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
52
- def setup(profile: str, ssh_key: str, aws_env: str):
55
+ @click.option('--1password', 'use_1password', is_flag=True, help='Use 1Password SSH agent for SSH authentication')
56
+ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1password: bool):
53
57
  """Set up AWS profile, SSH keys, and configuration for CloudX.
54
58
 
55
59
  This command will:
@@ -61,9 +65,17 @@ def setup(profile: str, ssh_key: str, aws_env: str):
61
65
  Example usage:
62
66
  cloudx-proxy setup
63
67
  cloudx-proxy setup --profile myprofile --ssh-key mykey
68
+ cloudx-proxy setup --ssh-config ~/.ssh/cloudx/config
69
+ cloudx-proxy setup --1password
64
70
  """
65
71
  try:
66
- setup = CloudXSetup(profile=profile, ssh_key=ssh_key, aws_env=aws_env)
72
+ setup = CloudXSetup(
73
+ profile=profile,
74
+ ssh_key=ssh_key,
75
+ ssh_config=ssh_config,
76
+ aws_env=aws_env,
77
+ use_1password=use_1password
78
+ )
67
79
 
68
80
  print("\n\033[1;95m=== cloudx-proxy Setup ===\033[0m\n")
69
81
 
@@ -7,7 +7,7 @@ from botocore.exceptions import ClientError
7
7
 
8
8
  class CloudXProxy:
9
9
  def __init__(self, instance_id: str, port: int = 22, profile: str = "vscode",
10
- region: str = None, ssh_key: str = "vscode", aws_env: str = None):
10
+ region: str = None, ssh_key: str = "vscode", ssh_config: str = None, aws_env: str = None):
11
11
  """Initialize CloudX client for SSH tunneling via AWS SSM.
12
12
 
13
13
  Args:
@@ -39,8 +39,14 @@ class CloudXProxy:
39
39
  self.ec2 = self.session.client('ec2')
40
40
  self.ec2_connect = self.session.client('ec2-instance-connect')
41
41
 
42
- # Set up SSH key path
43
- self.ssh_dir = os.path.expanduser("~/.ssh/vscode")
42
+ # Set up SSH configuration and key paths
43
+ if ssh_config:
44
+ self.ssh_config_file = os.path.expanduser(ssh_config)
45
+ self.ssh_dir = os.path.dirname(self.ssh_config_file)
46
+ else:
47
+ self.ssh_dir = os.path.expanduser("~/.ssh/vscode")
48
+ self.ssh_config_file = os.path.join(self.ssh_dir, "config")
49
+
44
50
  self.ssh_key = os.path.join(self.ssh_dir, f"{ssh_key}.pub")
45
51
 
46
52
  def log(self, message: str) -> None:
@@ -85,9 +91,21 @@ class CloudXProxy:
85
91
  return False
86
92
 
87
93
  def push_ssh_key(self) -> bool:
88
- """Push SSH public key to instance via EC2 Instance Connect."""
94
+ """Push SSH public key to instance via EC2 Instance Connect.
95
+
96
+ Determines which SSH key to use (regular key or 1Password-managed key),
97
+ then pushes the correct public key to the instance.
98
+ """
89
99
  try:
90
- with open(self.ssh_key) as f:
100
+ # Check if file exists with .pub extension (could be a non-1Password key)
101
+ # or if the .pub extension is already part of self.ssh_key (because of 1Password integration)
102
+ key_path = self.ssh_key
103
+ if not key_path.endswith('.pub'):
104
+ key_path += '.pub'
105
+
106
+ self.log(f"Using public key: {key_path}")
107
+
108
+ with open(key_path) as f:
91
109
  public_key = f.read()
92
110
 
93
111
  self.ec2_connect.send_ssh_public_key(
@@ -9,20 +9,32 @@ import boto3
9
9
  from botocore.exceptions import ClientError
10
10
 
11
11
  class CloudXSetup:
12
- def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", aws_env: str = None):
12
+ def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None,
13
+ aws_env: str = None, use_1password: bool = False):
13
14
  """Initialize cloudx-proxy setup.
14
15
 
15
16
  Args:
16
17
  profile: AWS profile name (default: "vscode")
17
18
  ssh_key: SSH key name (default: "vscode")
19
+ ssh_config: SSH config file path (default: None, uses ~/.ssh/vscode/config)
18
20
  aws_env: AWS environment directory (default: None)
21
+ use_1password: Use 1Password SSH agent for authentication (default: False)
19
22
  """
20
23
  self.profile = profile
21
24
  self.ssh_key = ssh_key
22
25
  self.aws_env = aws_env
26
+ self.use_1password = use_1password
23
27
  self.home_dir = str(Path.home())
24
- self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
25
- self.ssh_config_file = self.ssh_dir / "config"
28
+ self.onepassword_agent_sock = Path(self.home_dir) / ".1password" / "agent.sock"
29
+
30
+ # Set up ssh config paths based on provided config or default
31
+ if ssh_config:
32
+ self.ssh_config_file = Path(os.path.expanduser(ssh_config))
33
+ self.ssh_dir = self.ssh_config_file.parent
34
+ else:
35
+ self.ssh_dir = Path(self.home_dir) / ".ssh" / "vscode"
36
+ self.ssh_config_file = self.ssh_dir / "config"
37
+
26
38
  self.ssh_key_file = self.ssh_dir / f"{ssh_key}"
27
39
  self.default_env = None
28
40
 
@@ -143,6 +155,171 @@ class CloudXSetup:
143
155
  self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
144
156
  return False
145
157
 
158
+ def _check_1password_availability(self) -> bool:
159
+ """Check if 1Password CLI and SSH agent are available.
160
+
161
+ Returns:
162
+ bool: True if 1Password is available and configured
163
+ """
164
+ if not self.use_1password:
165
+ return False
166
+
167
+ self.print_status("Checking 1Password availability...")
168
+
169
+ # Check if 1Password CLI is installed
170
+ try:
171
+ result = subprocess.run(
172
+ ['op', '--version'],
173
+ capture_output=True,
174
+ text=True,
175
+ check=False
176
+ )
177
+ if result.returncode != 0:
178
+ self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
179
+ return False
180
+
181
+ self.print_status(f"1Password CLI {result.stdout.strip()} installed", True, 2)
182
+
183
+ # Check if 1Password SSH agent is running
184
+ agent_sock_exists = self.onepassword_agent_sock.exists()
185
+ if not agent_sock_exists:
186
+ self.print_status("1Password SSH agent socket not found at ~/.1password/agent.sock", False, 2)
187
+ self.print_status("Please ensure 1Password SSH agent is enabled in 1Password settings", None, 2)
188
+ return False
189
+
190
+ self.print_status("1Password SSH agent socket found", True, 2)
191
+
192
+ # Check if agent is active by trying to list identities
193
+ result = subprocess.run(
194
+ ['ssh-add', '-l'],
195
+ env={**os.environ, 'SSH_AUTH_SOCK': str(self.onepassword_agent_sock)},
196
+ capture_output=True,
197
+ text=True,
198
+ check=False
199
+ )
200
+
201
+ if "Could not open a connection to your authentication agent" in result.stderr:
202
+ self.print_status("1Password SSH agent is not running", False, 2)
203
+ return False
204
+
205
+ self.print_status("1Password SSH agent is running", True, 2)
206
+
207
+ # Check if 1Password CLI is authenticated
208
+ result = subprocess.run(
209
+ ['op', 'account', 'list'],
210
+ capture_output=True,
211
+ text=True,
212
+ check=False
213
+ )
214
+
215
+ if result.returncode != 0:
216
+ self.print_status("1Password CLI is not authenticated. Run 'op signin' first.", False, 2)
217
+ return False
218
+
219
+ self.print_status("1Password CLI is authenticated", True, 2)
220
+ return True
221
+
222
+ except FileNotFoundError:
223
+ self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
224
+ return False
225
+ except Exception as e:
226
+ self.print_status(f"Error checking 1Password: {str(e)}", False, 2)
227
+ return False
228
+
229
+ def _store_key_in_1password(self) -> bool:
230
+ """Store the SSH key in 1Password and offer to remove it from the filesystem.
231
+
232
+ Returns:
233
+ bool: True if successful
234
+ """
235
+ try:
236
+ # If key exists on filesystem, offer to store it in 1Password
237
+ key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
238
+
239
+ if not key_exists:
240
+ self.print_status("No SSH key found to store in 1Password", False, 2)
241
+ return False
242
+
243
+ # Ask if user wants to store the key in 1Password
244
+ store_in_1password = self.prompt("Would you like to store this SSH key in 1Password?", "Y").lower() == "y"
245
+ if not store_in_1password:
246
+ return True
247
+
248
+ # Get vault to store the key in
249
+ result = subprocess.run(
250
+ ['op', 'vault', 'list', '--format=json'],
251
+ capture_output=True,
252
+ text=True,
253
+ check=True
254
+ )
255
+
256
+ if result.returncode != 0:
257
+ self.print_status("Error listing 1Password vaults", False, 2)
258
+ return False
259
+
260
+ vaults = json.loads(result.stdout)
261
+ if not vaults:
262
+ self.print_status("No 1Password vaults found", False, 2)
263
+ return False
264
+
265
+ # Display available vaults
266
+ print("\n\033[96mAvailable 1Password vaults:\033[0m")
267
+ for i, vault in enumerate(vaults):
268
+ print(f" {i+1}. {vault['name']}")
269
+
270
+ # Let user select vault
271
+ vault_num = self.prompt("Select vault number to store SSH key", "1")
272
+ try:
273
+ vault_idx = int(vault_num) - 1
274
+ if vault_idx < 0 or vault_idx >= len(vaults):
275
+ self.print_status("Invalid vault number", False, 2)
276
+ return False
277
+ selected_vault = vaults[vault_idx]['id']
278
+ except ValueError:
279
+ self.print_status("Invalid input", False, 2)
280
+ return False
281
+
282
+ # Read private key
283
+ private_key = self.ssh_key_file.read_text()
284
+
285
+ # Create a title for the 1Password item
286
+ ssh_key_title = f"cloudX SSH Key - {self.ssh_key}"
287
+
288
+ # Create item in 1Password
289
+ # Format: op item create --category=sshkey --title="..." --vault="..." "privateKey=<private key>"
290
+ result = subprocess.run(
291
+ [
292
+ 'op', 'item', 'create',
293
+ '--category=sshkey',
294
+ f'--title={ssh_key_title}',
295
+ f'--vault={selected_vault}',
296
+ f'privateKey={private_key}'
297
+ ],
298
+ capture_output=True,
299
+ text=True,
300
+ check=False
301
+ )
302
+
303
+ if result.returncode != 0:
304
+ self.print_status(f"Error storing SSH key in 1Password: {result.stderr}", False, 2)
305
+ return False
306
+
307
+ self.print_status("SSH key stored in 1Password successfully", True, 2)
308
+
309
+ # Ask if user wants to remove private key from filesystem
310
+ remove_private_key = self.prompt("Would you like to remove the private key from the filesystem? (for security)", "Y").lower() == "y"
311
+ if remove_private_key:
312
+ # We should only remove the private key, keep the public key
313
+ self.ssh_key_file.unlink()
314
+ self.print_status("Private key removed from filesystem", True, 2)
315
+ self.print_status("\033[93mImportant: If you need to recover this key, it's now only available in 1Password\033[0m", None, 2)
316
+
317
+ return True
318
+
319
+ except Exception as e:
320
+ self.print_status(f"Error storing key in 1Password: {str(e)}", False, 2)
321
+ return False
322
+
146
323
  def setup_ssh_key(self) -> bool:
147
324
  """Set up SSH key pair.
148
325
 
@@ -150,6 +327,34 @@ class CloudXSetup:
150
327
  bool: True if key was set up successfully
151
328
  """
152
329
  self.print_header("SSH Key Configuration")
330
+
331
+ # Check 1Password integration if requested
332
+ if self.use_1password:
333
+ op_available = self._check_1password_availability()
334
+ if op_available:
335
+ self.print_status("Using 1Password SSH agent for authentication", True, 2)
336
+
337
+ # Generate a key if it doesn't exist
338
+ key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
339
+ if not key_exists:
340
+ self.print_status(f"No SSH key found for '{self.ssh_key}'", None, 2)
341
+ create_key = self.prompt("Would you like to create a new SSH key?", "Y").lower() == "y"
342
+ if create_key:
343
+ # We'll continue to the standard key creation flow
344
+ pass
345
+ else:
346
+ # User doesn't want a key, so we'll skip and just rely on 1Password
347
+ return True
348
+ else:
349
+ # Key exists, offer to store it in 1Password
350
+ self._store_key_in_1password()
351
+ return True
352
+ else:
353
+ proceed = self.prompt("1Password integration not available. Continue with standard SSH key setup?", "Y").lower() != "n"
354
+ if not proceed:
355
+ return False
356
+ self.use_1password = False # Fallback to standard setup
357
+
153
358
  self.print_status(f"Checking SSH key '{self.ssh_key}' configuration...")
154
359
 
155
360
  try:
@@ -188,6 +393,14 @@ class CloudXSetup:
188
393
  self.ssh_key_file.with_suffix('.pub').chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IROTH | stat.S_IRGRP) # 644 permissions
189
394
  self.print_status("Set key file permissions", True, 2)
190
395
 
396
+ # For both standard setup and 1Password when a key needs to be created,
397
+ # we'll use the same code to generate the key
398
+ self.print_status("Key generated successfully", True, 2)
399
+
400
+ # If using 1Password, offer to store the key there
401
+ if self.use_1password:
402
+ self._store_key_in_1password()
403
+
191
404
  return True
192
405
 
193
406
  except Exception as e:
@@ -198,6 +411,75 @@ class CloudXSetup:
198
411
  return True
199
412
  return False
200
413
 
414
+ def _build_proxy_command(self) -> str:
415
+ """Build the ProxyCommand with appropriate parameters.
416
+
417
+ Returns:
418
+ str: The complete ProxyCommand string
419
+ """
420
+ proxy_command = "uvx cloudx-proxy connect %h %p"
421
+ if self.profile != "vscode":
422
+ proxy_command += f" --profile {self.profile}"
423
+ if self.aws_env:
424
+ proxy_command += f" --aws-env {self.aws_env}"
425
+ if self.ssh_key != "vscode":
426
+ proxy_command += f" --ssh-key {self.ssh_key}"
427
+
428
+ return proxy_command
429
+
430
+ def _build_auth_config(self) -> str:
431
+ """Build the authentication configuration block.
432
+
433
+ Returns:
434
+ str: SSH config authentication section
435
+ """
436
+ if self.use_1password:
437
+ # When using 1Password:
438
+ # 1. Set IdentityAgent to the 1Password socket
439
+ # 2. Set IdentityFile to the PUBLIC key (.pub) to limit key search
440
+ # 3. Set IdentitiesOnly to yes to avoid using ssh-agent keys
441
+ return f""" IdentityAgent {self.onepassword_agent_sock}
442
+ IdentityFile {self.ssh_key_file}.pub
443
+ IdentitiesOnly yes
444
+ """
445
+ else:
446
+ # Standard SSH key configuration
447
+ return f""" IdentityFile {self.ssh_key_file}
448
+ IdentitiesOnly yes
449
+ """
450
+
451
+ def _build_host_config(self, cloudx_env: str, hostname: str, instance_id: str, include_proxy: bool = True) -> str:
452
+ """Build a host configuration block.
453
+
454
+ Args:
455
+ cloudx_env: CloudX environment
456
+ hostname: Hostname for the instance
457
+ instance_id: EC2 instance ID (None for wildcard entries)
458
+ include_proxy: Whether to include the ProxyCommand (default: True)
459
+
460
+ Returns:
461
+ str: Complete host configuration block
462
+ """
463
+ host_pattern = hostname if hostname else "*"
464
+ host_entry = f"""
465
+ Host cloudx-{cloudx_env}-{host_pattern}
466
+ """
467
+ # Add HostName only for specific hosts, not for wildcard entries
468
+ if instance_id:
469
+ host_entry += f""" HostName {instance_id}
470
+ """
471
+ host_entry += """ User ec2-user
472
+ """
473
+ # Add authentication configuration
474
+ host_entry += self._build_auth_config()
475
+
476
+ # Add proxy command if requested
477
+ if include_proxy:
478
+ host_entry += f""" ProxyCommand {self._build_proxy_command()}
479
+ """
480
+
481
+ return host_entry
482
+
201
483
  def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
202
484
  """Add settings to a specific host entry.
203
485
 
@@ -211,25 +493,8 @@ class CloudXSetup:
211
493
  bool: True if settings were added successfully
212
494
  """
213
495
  try:
214
- # Build host entry with all settings
215
- proxy_command = "uvx cloudx-proxy connect %h %p"
216
- if self.profile != "vscode":
217
- proxy_command += f" --profile {self.profile}"
218
- if self.aws_env:
219
- proxy_command += f" --aws-env {self.aws_env}"
220
- if self.ssh_key != "vscode":
221
- proxy_command += f" --ssh-key {self.ssh_key}"
222
-
223
- host_entry = f"""
224
- Host cloudx-{cloudx_env}-{hostname}
225
- HostName {instance_id}
226
- User ec2-user
227
- """
228
- host_entry += f""" IdentityFile {self.ssh_key_file}
229
- IdentitiesOnly yes
230
- """
231
- host_entry += f""" ProxyCommand {proxy_command}
232
- """
496
+ # Generate the host entry using the consolidated helper method
497
+ host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
233
498
 
234
499
  # Append host entry
235
500
  with open(self.ssh_config_file, 'a') as f:
@@ -330,8 +595,10 @@ Host cloudx-{cloudx_env}-{hostname}
330
595
  if f"Host cloudx-{cloudx_env}-*" in current_config:
331
596
  self.print_status(f"Found existing config for cloudx-{cloudx_env}-*", True, 2)
332
597
  choice = self.prompt(
333
- "Would you like to \n 1: override the existing config\n "
334
- " 2: add settings to the specific host entry?\nSelect an option ",
598
+ "Would you like to \n"
599
+ " 1: override the existing config\n"
600
+ " 2: add settings to the specific host entry?\n"
601
+ "Select an option",
335
602
  "1"
336
603
  )
337
604
  if choice == "2":
@@ -357,29 +624,18 @@ Host cloudx-{cloudx_env}-{hostname}
357
624
 
358
625
  # Create base config
359
626
  self.print_status(f"Creating new config for cloudx-{cloudx_env}-*", None, 2)
360
- # Build ProxyCommand with only non-default parameters
361
- proxy_command = "uvx cloudx-proxy connect %h %p"
362
- if self.profile != "vscode":
363
- proxy_command += f" --profile {self.profile}"
364
- if self.aws_env:
365
- proxy_command += f" --aws-env {self.aws_env}"
366
- if self.ssh_key != "vscode":
367
- proxy_command += f" --ssh-key {self.ssh_key}"
368
-
627
+
369
628
  # Ensure control directory exists with proper permissions
370
629
  if not self._ensure_control_dir():
371
630
  return False
372
631
 
373
- # Build base configuration
374
- base_config = f"""# cloudx-proxy SSH Configuration
375
- Host cloudx-{cloudx_env}-*
376
- User ec2-user
377
- """
378
- # Add key configuration
379
- base_config += f""" IdentityFile {self.ssh_key_file}
380
- IdentitiesOnly yes
381
-
382
- """
632
+ # Build base configuration with wildcard hostname pattern
633
+ # Start with a header comment
634
+ base_config = "# cloudx-proxy SSH Configuration\n"
635
+
636
+ # Add base host pattern with wildcard
637
+ base_config += self._build_host_config(cloudx_env, None, None, include_proxy=True)
638
+
383
639
  # Add SSH multiplexing configuration
384
640
  control_path = "~/.ssh/control/%r@%h:%p"
385
641
  if platform.system() == 'Windows':
@@ -391,9 +647,6 @@ Host cloudx-{cloudx_env}-*
391
647
  ControlPath {control_path}
392
648
  ControlPersist 4h
393
649
 
394
- """
395
- # Add ProxyCommand
396
- base_config += f""" ProxyCommand {proxy_command}
397
650
  """
398
651
 
399
652
  # If file exists, append the new config, otherwise create it
@@ -410,17 +663,16 @@ Host cloudx-{cloudx_env}-*
410
663
  self.ssh_config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions (owner read/write)
411
664
  self.print_status("Set config file permissions to 600", True, 2)
412
665
 
413
- # Add specific host entry
666
+ # Add specific host entry using the consolidated helper method
414
667
  self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
415
- host_entry = f"""
416
- Host cloudx-{cloudx_env}-{hostname}
417
- HostName {instance_id}
418
- """
668
+ host_entry = self._build_host_config(cloudx_env, hostname, instance_id, include_proxy=False)
419
669
  with open(self.ssh_config_file, 'a') as f:
420
670
  f.write(host_entry)
421
671
  self.print_status("Host entry added", True, 2)
422
672
 
423
- # Ensure main SSH config includes our config
673
+ # Handle system SSH config integration
674
+ system_config_path = Path(self.home_dir) / ".ssh" / "config"
675
+
424
676
  # Ensure ~/.ssh directory has proper permissions
425
677
  ssh_parent_dir = Path(self.home_dir) / ".ssh"
426
678
  if not ssh_parent_dir.exists():
@@ -428,36 +680,41 @@ Host cloudx-{cloudx_env}-{hostname}
428
680
  self.print_status(f"Created SSH directory: {ssh_parent_dir}", True, 2)
429
681
  self._set_directory_permissions(ssh_parent_dir)
430
682
 
431
- main_config = Path(self.home_dir) / ".ssh" / "config"
432
- include_line = f"Include {self.ssh_config_file}\n"
433
-
434
- if main_config.exists():
435
- content = main_config.read_text()
436
- if include_line not in content:
437
- with open(main_config, 'a') as f:
438
- f.write(f"\n{include_line}")
439
- self.print_status("Added include line to main SSH config", True, 2)
440
- else:
441
- self.print_status("Main SSH config already includes our config", True, 2)
442
-
443
- # Set correct permissions on main config file
444
- if platform.system() != 'Windows':
445
- import stat
446
- main_config.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
447
- self.print_status("Set main config file permissions to 600", True, 2)
683
+ # If our config file is the system config, we're done
684
+ if self.ssh_config_file.samefile(system_config_path) if self.ssh_config_file.exists() and system_config_path.exists() else str(self.ssh_config_file) == str(system_config_path):
685
+ self.print_status("Using system SSH config directly, no Include needed", True, 2)
448
686
  else:
449
- main_config.write_text(include_line)
450
- self.print_status("Created main SSH config with include line", True, 2)
687
+ # Otherwise, make sure the system config includes our config file
688
+ include_line = f"Include {self.ssh_config_file}\n"
451
689
 
452
- # Set correct permissions on newly created main config file
453
- if platform.system() != 'Windows':
454
- import stat
455
- main_config.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
456
- self.print_status("Set main config file permissions to 600", True, 2)
690
+ if system_config_path.exists():
691
+ content = system_config_path.read_text()
692
+ if include_line not in content:
693
+ with open(system_config_path, 'a') as f:
694
+ f.write(f"\n{include_line}")
695
+ self.print_status("Added include line to system SSH config", True, 2)
696
+ else:
697
+ self.print_status("System SSH config already includes our config", True, 2)
698
+
699
+ # Set correct permissions on system config file
700
+ if platform.system() != 'Windows':
701
+ import stat
702
+ system_config_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
703
+ self.print_status("Set system config file permissions to 600", True, 2)
704
+ else:
705
+ system_config_path.write_text(include_line)
706
+ self.print_status("Created system SSH config with include line", True, 2)
707
+
708
+ # Set correct permissions on newly created system config file
709
+ if platform.system() != 'Windows':
710
+ import stat
711
+ system_config_path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
712
+ self.print_status("Set system config file permissions to 600", True, 2)
457
713
 
458
714
  self.print_status("SSH configuration summary:", None)
459
- self.print_status(f"Main config: {main_config}", None, 2)
715
+ self.print_status(f"System config: {system_config_path}", None, 2)
460
716
  self.print_status(f"cloudx-proxy config: {self.ssh_config_file}", None, 2)
717
+ self.print_status(f"SSH key directory: {self.ssh_dir}", None, 2)
461
718
  self.print_status(f"Connect using: ssh cloudx-{cloudx_env}-{hostname}", None, 2)
462
719
 
463
720
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cloudx-proxy
3
- Version: 0.3.14
3
+ Version: 0.4.0
4
4
  Summary: SSH proxy command to connect VSCode with Cloud9/CloudX instance using AWS Systems Manager
5
5
  Author-email: easytocloud <info@easytocloud.com>
6
6
  License: MIT License
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes