ic-code 1.0.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.
- ic/__init__.py +29 -0
- ic/cli.py +769 -0
- ic/commands/__init__.py +10 -0
- ic/commands/config.py +699 -0
- ic/compat/__init__.py +241 -0
- ic/compat/cli.py +289 -0
- ic/compat/common.py +243 -0
- ic/config/__init__.py +10 -0
- ic/config/cleanup.py +382 -0
- ic/config/docs_organizer.py +587 -0
- ic/config/external.py +456 -0
- ic/config/manager.py +898 -0
- ic/config/migration.py +628 -0
- ic/config/schema.py +595 -0
- ic/config/secrets.py +437 -0
- ic/config/security.py +462 -0
- ic/core/__init__.py +16 -0
- ic/core/logging.py +311 -0
- ic/core/mcp_manager.py +856 -0
- ic/core/session.py +392 -0
- ic/core/silence_logging.py +67 -0
- ic_code-1.0.0.dist-info/METADATA +354 -0
- ic_code-1.0.0.dist-info/RECORD +27 -0
- ic_code-1.0.0.dist-info/WHEEL +5 -0
- ic_code-1.0.0.dist-info/entry_points.txt +2 -0
- ic_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- ic_code-1.0.0.dist-info/top_level.txt +1 -0
ic/config/external.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""
|
|
2
|
+
External configuration loader module for IC.
|
|
3
|
+
|
|
4
|
+
This module provides loading of external configuration files from various cloud providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import configparser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExternalConfigLoader:
|
|
16
|
+
"""
|
|
17
|
+
Loads external configuration files from various sources.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config_manager=None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize ExternalConfigLoader.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config_manager: Reference to ConfigManager instance
|
|
26
|
+
"""
|
|
27
|
+
self.config_manager = config_manager
|
|
28
|
+
self.external_configs: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
def load_all_external_configs(self) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Load all external configuration files.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dictionary containing all external configurations
|
|
36
|
+
"""
|
|
37
|
+
external_configs = {}
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Load AWS configuration
|
|
41
|
+
aws_config = self.load_aws_config()
|
|
42
|
+
if aws_config:
|
|
43
|
+
external_configs['aws'] = aws_config
|
|
44
|
+
|
|
45
|
+
# Load OCI configuration
|
|
46
|
+
oci_config = self.load_oci_config()
|
|
47
|
+
if oci_config:
|
|
48
|
+
external_configs['oci'] = oci_config
|
|
49
|
+
|
|
50
|
+
# Load SSH configuration
|
|
51
|
+
ssh_config = self.load_ssh_config()
|
|
52
|
+
if ssh_config:
|
|
53
|
+
external_configs['ssh'] = ssh_config
|
|
54
|
+
|
|
55
|
+
# Load CloudFlare configuration
|
|
56
|
+
cf_config = self.load_cloudflare_config()
|
|
57
|
+
if cf_config:
|
|
58
|
+
external_configs['cloudflare'] = cf_config
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(f"Failed to load some external configurations: {e}")
|
|
62
|
+
|
|
63
|
+
self.external_configs = external_configs
|
|
64
|
+
return external_configs
|
|
65
|
+
|
|
66
|
+
def load_aws_config(self) -> Dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Load AWS configuration from ~/.aws/config and ~/.aws/credentials.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary containing AWS configuration
|
|
72
|
+
"""
|
|
73
|
+
aws_config = {}
|
|
74
|
+
|
|
75
|
+
# Load AWS config file
|
|
76
|
+
aws_config_path = Path.home() / ".aws" / "config"
|
|
77
|
+
if aws_config_path.exists():
|
|
78
|
+
try:
|
|
79
|
+
config = configparser.ConfigParser()
|
|
80
|
+
config.read(aws_config_path)
|
|
81
|
+
|
|
82
|
+
profiles = {}
|
|
83
|
+
for section_name in config.sections():
|
|
84
|
+
if section_name.startswith('profile '):
|
|
85
|
+
profile_name = section_name.split('profile ')[1]
|
|
86
|
+
profiles[profile_name] = dict(config[section_name])
|
|
87
|
+
elif section_name == 'default':
|
|
88
|
+
profiles['default'] = dict(config[section_name])
|
|
89
|
+
|
|
90
|
+
if profiles:
|
|
91
|
+
aws_config['profiles'] = profiles
|
|
92
|
+
logger.debug(f"Loaded {len(profiles)} AWS profiles from config")
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Failed to load AWS config: {e}")
|
|
96
|
+
else:
|
|
97
|
+
logger.debug("AWS config file not found at ~/.aws/config")
|
|
98
|
+
|
|
99
|
+
# Load AWS credentials file
|
|
100
|
+
aws_creds_path = Path.home() / ".aws" / "credentials"
|
|
101
|
+
if aws_creds_path.exists():
|
|
102
|
+
try:
|
|
103
|
+
config = configparser.ConfigParser()
|
|
104
|
+
config.read(aws_creds_path)
|
|
105
|
+
|
|
106
|
+
credentials = {}
|
|
107
|
+
for section_name in config.sections():
|
|
108
|
+
# Don't store actual credentials, just metadata
|
|
109
|
+
credentials[section_name] = {
|
|
110
|
+
'has_access_key': 'aws_access_key_id' in config[section_name],
|
|
111
|
+
'has_secret_key': 'aws_secret_access_key' in config[section_name],
|
|
112
|
+
'has_session_token': 'aws_session_token' in config[section_name]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if credentials:
|
|
116
|
+
aws_config['credentials_profiles'] = credentials
|
|
117
|
+
logger.debug(f"Found {len(credentials)} AWS credential profiles")
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Failed to load AWS credentials metadata: {e}")
|
|
121
|
+
else:
|
|
122
|
+
logger.debug("AWS credentials file not found at ~/.aws/credentials")
|
|
123
|
+
|
|
124
|
+
return aws_config
|
|
125
|
+
|
|
126
|
+
def load_oci_config(self) -> Dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Load OCI configuration from ~/.oci/config.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary containing OCI configuration
|
|
132
|
+
"""
|
|
133
|
+
oci_config = {}
|
|
134
|
+
|
|
135
|
+
oci_config_path = Path.home() / ".oci" / "config"
|
|
136
|
+
if oci_config_path.exists():
|
|
137
|
+
try:
|
|
138
|
+
config = configparser.ConfigParser()
|
|
139
|
+
config.read(oci_config_path)
|
|
140
|
+
|
|
141
|
+
profiles = {}
|
|
142
|
+
for section_name in config.sections():
|
|
143
|
+
# Store non-sensitive configuration only
|
|
144
|
+
profile_config = {}
|
|
145
|
+
for key, value in config[section_name].items():
|
|
146
|
+
# Skip sensitive keys like private keys
|
|
147
|
+
if 'key' not in key.lower() or key.lower() in ['key_file', 'key_path']:
|
|
148
|
+
if key.lower() in ['key_file', 'key_path']:
|
|
149
|
+
# Just indicate that key file exists, don't store path
|
|
150
|
+
profile_config[key] = "***KEY_FILE_CONFIGURED***" if Path(value).exists() else "***KEY_FILE_MISSING***"
|
|
151
|
+
else:
|
|
152
|
+
profile_config[key] = value
|
|
153
|
+
|
|
154
|
+
profiles[section_name] = profile_config
|
|
155
|
+
|
|
156
|
+
if profiles:
|
|
157
|
+
oci_config['profiles'] = profiles
|
|
158
|
+
logger.debug(f"Loaded {len(profiles)} OCI profiles from config")
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.warning(f"Failed to load OCI config: {e}")
|
|
162
|
+
else:
|
|
163
|
+
logger.debug("OCI config file not found at ~/.oci/config")
|
|
164
|
+
|
|
165
|
+
return oci_config
|
|
166
|
+
|
|
167
|
+
def load_ssh_config(self) -> Dict[str, Any]:
|
|
168
|
+
"""
|
|
169
|
+
Load SSH configuration from ~/.ssh/config.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary containing SSH configuration
|
|
173
|
+
"""
|
|
174
|
+
ssh_config = {}
|
|
175
|
+
|
|
176
|
+
ssh_config_path = Path.home() / ".ssh" / "config"
|
|
177
|
+
if ssh_config_path.exists():
|
|
178
|
+
try:
|
|
179
|
+
hosts = {}
|
|
180
|
+
current_host = None
|
|
181
|
+
|
|
182
|
+
with open(ssh_config_path, 'r') as f:
|
|
183
|
+
for line in f:
|
|
184
|
+
line = line.strip()
|
|
185
|
+
if not line or line.startswith('#'):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
if line.lower().startswith('host '):
|
|
189
|
+
current_host = line.split(' ', 1)[1]
|
|
190
|
+
hosts[current_host] = {}
|
|
191
|
+
elif current_host and ' ' in line:
|
|
192
|
+
key, value = line.split(' ', 1)
|
|
193
|
+
key_lower = key.lower()
|
|
194
|
+
|
|
195
|
+
# Store configuration but mask sensitive information
|
|
196
|
+
if 'identityfile' in key_lower:
|
|
197
|
+
# Check if identity file exists
|
|
198
|
+
identity_path = Path(value).expanduser()
|
|
199
|
+
hosts[current_host][key_lower] = "***IDENTITY_FILE_CONFIGURED***" if identity_path.exists() else "***IDENTITY_FILE_MISSING***"
|
|
200
|
+
else:
|
|
201
|
+
hosts[current_host][key_lower] = value
|
|
202
|
+
|
|
203
|
+
if hosts:
|
|
204
|
+
ssh_config['hosts'] = hosts
|
|
205
|
+
logger.debug(f"Loaded {len(hosts)} SSH host configurations")
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.warning(f"Failed to load SSH config: {e}")
|
|
209
|
+
else:
|
|
210
|
+
logger.debug("SSH config file not found at ~/.ssh/config")
|
|
211
|
+
|
|
212
|
+
return ssh_config
|
|
213
|
+
|
|
214
|
+
def load_cloudflare_config(self) -> Dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
Load CloudFlare configuration from various possible locations.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dictionary containing CloudFlare configuration
|
|
220
|
+
"""
|
|
221
|
+
cf_config = {}
|
|
222
|
+
|
|
223
|
+
# Check for CloudFlare config in various locations
|
|
224
|
+
possible_paths = [
|
|
225
|
+
Path.home() / ".cloudflare" / "config",
|
|
226
|
+
Path.home() / ".cloudflare" / "config.yaml",
|
|
227
|
+
Path.home() / ".cloudflare" / "config.yml",
|
|
228
|
+
Path("config") / "cloudflare.yaml",
|
|
229
|
+
Path("config") / "cloudflare.yml"
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
for cf_path in possible_paths:
|
|
233
|
+
if cf_path.exists():
|
|
234
|
+
try:
|
|
235
|
+
if cf_path.suffix.lower() in ['.yaml', '.yml']:
|
|
236
|
+
# Load YAML format
|
|
237
|
+
import yaml
|
|
238
|
+
with open(cf_path, 'r') as f:
|
|
239
|
+
cf_data = yaml.safe_load(f) or {}
|
|
240
|
+
|
|
241
|
+
# Mask sensitive information
|
|
242
|
+
cf_config = self._mask_cloudflare_secrets(cf_data)
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
# Try to parse as simple key=value format
|
|
246
|
+
with open(cf_path, 'r') as f:
|
|
247
|
+
for line in f:
|
|
248
|
+
line = line.strip()
|
|
249
|
+
if '=' in line and not line.startswith('#'):
|
|
250
|
+
key, value = line.split('=', 1)
|
|
251
|
+
key = key.strip()
|
|
252
|
+
value = value.strip()
|
|
253
|
+
|
|
254
|
+
# Mask sensitive values
|
|
255
|
+
if any(sensitive in key.lower() for sensitive in ['token', 'key', 'secret', 'password']):
|
|
256
|
+
cf_config[key] = "***MASKED***"
|
|
257
|
+
else:
|
|
258
|
+
cf_config[key] = value
|
|
259
|
+
|
|
260
|
+
if cf_config:
|
|
261
|
+
logger.debug(f"Loaded CloudFlare config from {cf_path}")
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"Failed to load CloudFlare config from {cf_path}: {e}")
|
|
266
|
+
|
|
267
|
+
if not cf_config:
|
|
268
|
+
logger.debug("No CloudFlare config file found")
|
|
269
|
+
|
|
270
|
+
return cf_config
|
|
271
|
+
|
|
272
|
+
def _mask_cloudflare_secrets(self, cf_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
273
|
+
"""
|
|
274
|
+
Mask sensitive information in CloudFlare configuration.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
cf_data: CloudFlare configuration data
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Configuration with sensitive data masked
|
|
281
|
+
"""
|
|
282
|
+
masked_data = {}
|
|
283
|
+
|
|
284
|
+
for key, value in cf_data.items():
|
|
285
|
+
if isinstance(value, dict):
|
|
286
|
+
masked_data[key] = self._mask_cloudflare_secrets(value)
|
|
287
|
+
elif isinstance(value, str) and any(sensitive in key.lower() for sensitive in ['token', 'key', 'secret', 'password']):
|
|
288
|
+
if value:
|
|
289
|
+
masked_data[key] = "***MASKED***"
|
|
290
|
+
else:
|
|
291
|
+
masked_data[key] = value
|
|
292
|
+
else:
|
|
293
|
+
masked_data[key] = value
|
|
294
|
+
|
|
295
|
+
return masked_data
|
|
296
|
+
|
|
297
|
+
def get_external_config_value(self, service: str, key_path: str, default: Any = None) -> Any:
|
|
298
|
+
"""
|
|
299
|
+
Get a value from external configuration using dot notation.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
service: Service name (aws, oci, ssh, cloudflare)
|
|
303
|
+
key_path: Dot-separated path to configuration value
|
|
304
|
+
default: Default value if key is not found
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Configuration value or default
|
|
308
|
+
"""
|
|
309
|
+
if service not in self.external_configs:
|
|
310
|
+
return default
|
|
311
|
+
|
|
312
|
+
keys = key_path.split('.')
|
|
313
|
+
current = self.external_configs[service]
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
for key in keys:
|
|
317
|
+
current = current[key]
|
|
318
|
+
return current
|
|
319
|
+
except (KeyError, TypeError):
|
|
320
|
+
return default
|
|
321
|
+
|
|
322
|
+
def has_external_config(self, service: str) -> bool:
|
|
323
|
+
"""
|
|
324
|
+
Check if external configuration exists for a service.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
service: Service name to check
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
True if external configuration exists
|
|
331
|
+
"""
|
|
332
|
+
return service in self.external_configs and bool(self.external_configs[service])
|
|
333
|
+
|
|
334
|
+
def get_aws_profile_names(self) -> list:
|
|
335
|
+
"""
|
|
336
|
+
Get list of available AWS profile names.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
List of AWS profile names
|
|
340
|
+
"""
|
|
341
|
+
aws_config = self.external_configs.get('aws', {})
|
|
342
|
+
profiles = aws_config.get('profiles', {})
|
|
343
|
+
return list(profiles.keys())
|
|
344
|
+
|
|
345
|
+
def get_oci_profile_names(self) -> list:
|
|
346
|
+
"""
|
|
347
|
+
Get list of available OCI profile names.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of OCI profile names
|
|
351
|
+
"""
|
|
352
|
+
oci_config = self.external_configs.get('oci', {})
|
|
353
|
+
profiles = oci_config.get('profiles', {})
|
|
354
|
+
return list(profiles.keys())
|
|
355
|
+
|
|
356
|
+
def get_ssh_host_names(self) -> list:
|
|
357
|
+
"""
|
|
358
|
+
Get list of configured SSH host names.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
List of SSH host names
|
|
362
|
+
"""
|
|
363
|
+
ssh_config = self.external_configs.get('ssh', {})
|
|
364
|
+
hosts = ssh_config.get('hosts', {})
|
|
365
|
+
return list(hosts.keys())
|
|
366
|
+
|
|
367
|
+
def validate_external_configs(self) -> Dict[str, List[str]]:
|
|
368
|
+
"""
|
|
369
|
+
Validate external configurations and return any issues found.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Dictionary mapping service names to lists of validation issues
|
|
373
|
+
"""
|
|
374
|
+
issues = {}
|
|
375
|
+
|
|
376
|
+
# Validate AWS configuration
|
|
377
|
+
aws_issues = self._validate_aws_config()
|
|
378
|
+
if aws_issues:
|
|
379
|
+
issues['aws'] = aws_issues
|
|
380
|
+
|
|
381
|
+
# Validate OCI configuration
|
|
382
|
+
oci_issues = self._validate_oci_config()
|
|
383
|
+
if oci_issues:
|
|
384
|
+
issues['oci'] = oci_issues
|
|
385
|
+
|
|
386
|
+
# Validate SSH configuration
|
|
387
|
+
ssh_issues = self._validate_ssh_config()
|
|
388
|
+
if ssh_issues:
|
|
389
|
+
issues['ssh'] = ssh_issues
|
|
390
|
+
|
|
391
|
+
return issues
|
|
392
|
+
|
|
393
|
+
def _validate_aws_config(self) -> List[str]:
|
|
394
|
+
"""Validate AWS configuration."""
|
|
395
|
+
issues = []
|
|
396
|
+
aws_config = self.external_configs.get('aws', {})
|
|
397
|
+
|
|
398
|
+
if not aws_config:
|
|
399
|
+
issues.append("No AWS configuration found")
|
|
400
|
+
return issues
|
|
401
|
+
|
|
402
|
+
profiles = aws_config.get('profiles', {})
|
|
403
|
+
credentials = aws_config.get('credentials_profiles', {})
|
|
404
|
+
|
|
405
|
+
if not profiles and not credentials:
|
|
406
|
+
issues.append("No AWS profiles or credentials found")
|
|
407
|
+
|
|
408
|
+
# Check for profiles without corresponding credentials
|
|
409
|
+
for profile_name in profiles.keys():
|
|
410
|
+
if profile_name not in credentials:
|
|
411
|
+
issues.append(f"AWS profile '{profile_name}' has no corresponding credentials")
|
|
412
|
+
|
|
413
|
+
return issues
|
|
414
|
+
|
|
415
|
+
def _validate_oci_config(self) -> List[str]:
|
|
416
|
+
"""Validate OCI configuration."""
|
|
417
|
+
issues = []
|
|
418
|
+
oci_config = self.external_configs.get('oci', {})
|
|
419
|
+
|
|
420
|
+
if not oci_config:
|
|
421
|
+
issues.append("No OCI configuration found")
|
|
422
|
+
return issues
|
|
423
|
+
|
|
424
|
+
profiles = oci_config.get('profiles', {})
|
|
425
|
+
|
|
426
|
+
for profile_name, profile_config in profiles.items():
|
|
427
|
+
required_fields = ['user', 'fingerprint', 'tenancy', 'region']
|
|
428
|
+
for field in required_fields:
|
|
429
|
+
if field not in profile_config:
|
|
430
|
+
issues.append(f"OCI profile '{profile_name}' missing required field '{field}'")
|
|
431
|
+
|
|
432
|
+
# Check if key file is configured and exists
|
|
433
|
+
key_file_status = profile_config.get('key_file', '')
|
|
434
|
+
if 'MISSING' in key_file_status:
|
|
435
|
+
issues.append(f"OCI profile '{profile_name}' key file is missing")
|
|
436
|
+
|
|
437
|
+
return issues
|
|
438
|
+
|
|
439
|
+
def _validate_ssh_config(self) -> List[str]:
|
|
440
|
+
"""Validate SSH configuration."""
|
|
441
|
+
issues = []
|
|
442
|
+
ssh_config = self.external_configs.get('ssh', {})
|
|
443
|
+
|
|
444
|
+
if not ssh_config:
|
|
445
|
+
issues.append("No SSH configuration found")
|
|
446
|
+
return issues
|
|
447
|
+
|
|
448
|
+
hosts = ssh_config.get('hosts', {})
|
|
449
|
+
|
|
450
|
+
for host_name, host_config in hosts.items():
|
|
451
|
+
# Check if identity file exists
|
|
452
|
+
identity_status = host_config.get('identityfile', '')
|
|
453
|
+
if 'MISSING' in identity_status:
|
|
454
|
+
issues.append(f"SSH host '{host_name}' identity file is missing")
|
|
455
|
+
|
|
456
|
+
return issues
|