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/migration.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration manager module for IC.
|
|
3
|
+
|
|
4
|
+
This module provides migration functionality from .env files to YAML configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MigrationManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages migration from .env files to YAML configuration system.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config_manager=None):
|
|
23
|
+
"""
|
|
24
|
+
Initialize MigrationManager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config_manager: Reference to ConfigManager instance
|
|
28
|
+
"""
|
|
29
|
+
self.config_manager = config_manager
|
|
30
|
+
self.backup_dir = Path("backup")
|
|
31
|
+
self.migration_history: List[Dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
def migrate_env_to_yaml(self, env_file_path: str = ".env",
|
|
34
|
+
force: bool = False) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Migrate configuration from .env file to YAML format.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
env_file_path: Path to the .env file
|
|
40
|
+
force: Force migration even if YAML files already exist
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if migration was successful
|
|
44
|
+
"""
|
|
45
|
+
env_path = Path(env_file_path)
|
|
46
|
+
if not env_path.exists():
|
|
47
|
+
logger.warning(f"No .env file found at {env_path}")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
# Check if YAML files already exist
|
|
51
|
+
config_dir = Path("config")
|
|
52
|
+
default_yaml = config_dir / "default.yaml"
|
|
53
|
+
secrets_yaml = config_dir / "secrets.yaml"
|
|
54
|
+
|
|
55
|
+
if not force and (default_yaml.exists() or secrets_yaml.exists()):
|
|
56
|
+
logger.warning("YAML configuration files already exist. Use force=True to overwrite.")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Parse .env file
|
|
61
|
+
env_vars = self._parse_env_file(env_path)
|
|
62
|
+
|
|
63
|
+
# Separate sensitive and non-sensitive data
|
|
64
|
+
default_config, secrets_config = self._categorize_env_vars(env_vars)
|
|
65
|
+
|
|
66
|
+
# Create config directory
|
|
67
|
+
config_dir.mkdir(exist_ok=True)
|
|
68
|
+
|
|
69
|
+
# Backup existing files if they exist
|
|
70
|
+
if default_yaml.exists():
|
|
71
|
+
self._backup_file(default_yaml)
|
|
72
|
+
if secrets_yaml.exists():
|
|
73
|
+
self._backup_file(secrets_yaml)
|
|
74
|
+
|
|
75
|
+
# Save configurations
|
|
76
|
+
success = True
|
|
77
|
+
|
|
78
|
+
# Save default configuration
|
|
79
|
+
if default_config:
|
|
80
|
+
success &= self._save_yaml_config(default_yaml, default_config)
|
|
81
|
+
|
|
82
|
+
# Save secrets configuration
|
|
83
|
+
if secrets_config:
|
|
84
|
+
success &= self._save_yaml_config(secrets_yaml, secrets_config)
|
|
85
|
+
|
|
86
|
+
# Set restrictive permissions on secrets file
|
|
87
|
+
try:
|
|
88
|
+
secrets_yaml.chmod(0o600)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning(f"Failed to set restrictive permissions on secrets file: {e}")
|
|
91
|
+
|
|
92
|
+
if success:
|
|
93
|
+
# Backup original .env file
|
|
94
|
+
self._backup_file(env_path)
|
|
95
|
+
|
|
96
|
+
# Record migration
|
|
97
|
+
self._record_migration(env_path, default_yaml, secrets_yaml)
|
|
98
|
+
|
|
99
|
+
logger.info("Successfully migrated .env file to YAML configuration")
|
|
100
|
+
return True
|
|
101
|
+
else:
|
|
102
|
+
logger.error("Failed to save YAML configuration files")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to migrate .env file: {e}")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def _parse_env_file(self, env_path: Path) -> Dict[str, str]:
|
|
110
|
+
"""
|
|
111
|
+
Parse .env file and extract key-value pairs.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
env_path: Path to .env file
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary of environment variables
|
|
118
|
+
"""
|
|
119
|
+
env_vars = {}
|
|
120
|
+
|
|
121
|
+
with open(env_path, 'r', encoding='utf-8') as f:
|
|
122
|
+
for line_num, line in enumerate(f, 1):
|
|
123
|
+
line = line.strip()
|
|
124
|
+
|
|
125
|
+
# Skip empty lines and comments
|
|
126
|
+
if not line or line.startswith('#'):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# Handle lines with equals sign
|
|
130
|
+
if '=' in line:
|
|
131
|
+
key, value = line.split('=', 1)
|
|
132
|
+
key = key.strip()
|
|
133
|
+
value = value.strip()
|
|
134
|
+
|
|
135
|
+
# Remove quotes if present
|
|
136
|
+
if value.startswith('"') and value.endswith('"'):
|
|
137
|
+
value = value[1:-1]
|
|
138
|
+
elif value.startswith("'") and value.endswith("'"):
|
|
139
|
+
value = value[1:-1]
|
|
140
|
+
|
|
141
|
+
env_vars[key] = value
|
|
142
|
+
else:
|
|
143
|
+
logger.warning(f"Skipping invalid line {line_num} in {env_path}: {line}")
|
|
144
|
+
|
|
145
|
+
return env_vars
|
|
146
|
+
|
|
147
|
+
def _categorize_env_vars(self, env_vars: Dict[str, str]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
148
|
+
"""
|
|
149
|
+
Categorize environment variables into default config and secrets.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
env_vars: Dictionary of environment variables
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Tuple of (default_config, secrets_config)
|
|
156
|
+
"""
|
|
157
|
+
# Start with base default configuration
|
|
158
|
+
default_config = self._get_base_default_config()
|
|
159
|
+
secrets_config = {"version": "2.0"}
|
|
160
|
+
|
|
161
|
+
# Define sensitive keys
|
|
162
|
+
sensitive_keys = {
|
|
163
|
+
'SLACK_WEBHOOK_URL', 'CLOUDFLARE_EMAIL', 'CLOUDFLARE_API_TOKEN',
|
|
164
|
+
'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET',
|
|
165
|
+
'GCP_SERVICE_ACCOUNT_KEY_PATH', 'GOOGLE_APPLICATION_CREDENTIALS',
|
|
166
|
+
'AWS_ACCOUNTS' # Account IDs are considered sensitive
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Categorize each environment variable
|
|
170
|
+
for key, value in env_vars.items():
|
|
171
|
+
if key in sensitive_keys or self._is_sensitive_key(key):
|
|
172
|
+
self._add_to_secrets_config(secrets_config, key, value)
|
|
173
|
+
else:
|
|
174
|
+
self._add_to_default_config(default_config, key, value)
|
|
175
|
+
|
|
176
|
+
return default_config, secrets_config
|
|
177
|
+
|
|
178
|
+
def _is_sensitive_key(self, key: str) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Check if a key is potentially sensitive.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
key: Environment variable key
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if key is potentially sensitive
|
|
187
|
+
"""
|
|
188
|
+
sensitive_patterns = [
|
|
189
|
+
'token', 'key', 'secret', 'password', 'passwd', 'pwd',
|
|
190
|
+
'credential', 'webhook', 'api_key', 'access_key', 'private_key'
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
key_lower = key.lower()
|
|
194
|
+
return any(pattern in key_lower for pattern in sensitive_patterns)
|
|
195
|
+
|
|
196
|
+
def _add_to_secrets_config(self, secrets_config: Dict[str, Any], key: str, value: str):
|
|
197
|
+
"""Add environment variable to secrets configuration."""
|
|
198
|
+
if key == 'AWS_ACCOUNTS':
|
|
199
|
+
self._set_nested_value(secrets_config, ['aws', 'accounts'],
|
|
200
|
+
[acc.strip() for acc in value.split(',') if acc.strip()])
|
|
201
|
+
elif key == 'CLOUDFLARE_EMAIL':
|
|
202
|
+
self._set_nested_value(secrets_config, ['cloudflare', 'email'], value)
|
|
203
|
+
elif key == 'CLOUDFLARE_API_TOKEN':
|
|
204
|
+
self._set_nested_value(secrets_config, ['cloudflare', 'api_token'], value)
|
|
205
|
+
elif key == 'CLOUDFLARE_ACCOUNTS':
|
|
206
|
+
self._set_nested_value(secrets_config, ['cloudflare', 'accounts'],
|
|
207
|
+
[acc.strip() for acc in value.split(',') if acc.strip()])
|
|
208
|
+
elif key == 'CLOUDFLARE_ZONES':
|
|
209
|
+
self._set_nested_value(secrets_config, ['cloudflare', 'zones'],
|
|
210
|
+
[zone.strip() for zone in value.split(',') if zone.strip()])
|
|
211
|
+
elif key == 'SLACK_WEBHOOK_URL':
|
|
212
|
+
self._set_nested_value(secrets_config, ['slack', 'webhook_url'], value)
|
|
213
|
+
elif key == 'GCP_SERVICE_ACCOUNT_KEY_PATH' or key == 'GOOGLE_APPLICATION_CREDENTIALS':
|
|
214
|
+
self._set_nested_value(secrets_config, ['gcp', 'service_account_key_path'], value)
|
|
215
|
+
elif key == 'GCP_PROJECTS':
|
|
216
|
+
self._set_nested_value(secrets_config, ['gcp', 'projects'],
|
|
217
|
+
[proj.strip() for proj in value.split(',') if proj.strip()])
|
|
218
|
+
elif key == 'AZURE_TENANT_ID':
|
|
219
|
+
self._set_nested_value(secrets_config, ['azure', 'tenant_id'], value)
|
|
220
|
+
elif key == 'AZURE_CLIENT_ID':
|
|
221
|
+
self._set_nested_value(secrets_config, ['azure', 'client_id'], value)
|
|
222
|
+
elif key == 'AZURE_CLIENT_SECRET':
|
|
223
|
+
self._set_nested_value(secrets_config, ['azure', 'client_secret'], value)
|
|
224
|
+
elif key == 'AZURE_SUBSCRIPTIONS':
|
|
225
|
+
self._set_nested_value(secrets_config, ['azure', 'subscriptions'],
|
|
226
|
+
[sub.strip() for sub in value.split(',') if sub.strip()])
|
|
227
|
+
else:
|
|
228
|
+
# Generic sensitive key handling
|
|
229
|
+
logger.warning(f"Unknown sensitive key '{key}', storing in secrets under 'other'")
|
|
230
|
+
self._set_nested_value(secrets_config, ['other', key.lower()], value)
|
|
231
|
+
|
|
232
|
+
def _add_to_default_config(self, default_config: Dict[str, Any], key: str, value: str):
|
|
233
|
+
"""Add environment variable to default configuration."""
|
|
234
|
+
if key == 'REGIONS':
|
|
235
|
+
self._set_nested_value(default_config, ['aws', 'regions'],
|
|
236
|
+
[reg.strip() for reg in value.split(',') if reg.strip()])
|
|
237
|
+
elif key == 'REQUIRED_TAGS':
|
|
238
|
+
self._set_nested_value(default_config, ['aws', 'tags', 'required'],
|
|
239
|
+
[tag.strip() for tag in value.split(',') if tag.strip()])
|
|
240
|
+
elif key == 'OPTIONAL_TAGS':
|
|
241
|
+
self._set_nested_value(default_config, ['aws', 'tags', 'optional'],
|
|
242
|
+
[tag.strip() for tag in value.split(',') if tag.strip()])
|
|
243
|
+
elif key.startswith('RULE_'):
|
|
244
|
+
rule_name = key[5:].lower() # Remove 'RULE_' prefix
|
|
245
|
+
self._set_nested_value(default_config, ['aws', 'tags', 'rules', rule_name.title()], value)
|
|
246
|
+
elif key == 'SSH_MAX_WORKER':
|
|
247
|
+
self._set_nested_value(default_config, ['ssh', 'max_workers'], int(value))
|
|
248
|
+
elif key == 'SSH_SKIP_PREFIXES':
|
|
249
|
+
self._set_nested_value(default_config, ['ssh', 'skip_prefixes'],
|
|
250
|
+
[prefix.strip() for prefix in value.split(',') if prefix.strip()])
|
|
251
|
+
elif key == 'PORT_OPEN_TIMEOUT':
|
|
252
|
+
self._set_nested_value(default_config, ['ssh', 'timeouts', 'port_scan'], float(value))
|
|
253
|
+
elif key == 'SSH_TIMEOUT':
|
|
254
|
+
self._set_nested_value(default_config, ['ssh', 'timeouts', 'ssh_connect'], int(value))
|
|
255
|
+
elif key == 'SSH_KEY_DIR':
|
|
256
|
+
self._set_nested_value(default_config, ['ssh', 'key_dir'], value)
|
|
257
|
+
elif key == 'SSH_CONFIG_FILE':
|
|
258
|
+
self._set_nested_value(default_config, ['ssh', 'config_file'], value)
|
|
259
|
+
elif key == 'LOG_LEVEL':
|
|
260
|
+
self._set_nested_value(default_config, ['logging', 'console_level'], value.upper())
|
|
261
|
+
elif key == 'OCI_CONFIG_PATH':
|
|
262
|
+
self._set_nested_value(default_config, ['oci', 'config_path'], value)
|
|
263
|
+
elif key == 'GCP_REGIONS':
|
|
264
|
+
self._set_nested_value(default_config, ['gcp', 'regions'],
|
|
265
|
+
[reg.strip() for reg in value.split(',') if reg.strip()])
|
|
266
|
+
elif key == 'GCP_ZONES':
|
|
267
|
+
self._set_nested_value(default_config, ['gcp', 'zones'],
|
|
268
|
+
[zone.strip() for zone in value.split(',') if zone.strip()])
|
|
269
|
+
elif key == 'GCP_MAX_WORKERS':
|
|
270
|
+
self._set_nested_value(default_config, ['gcp', 'max_workers'], int(value))
|
|
271
|
+
elif key == 'AZURE_LOCATIONS':
|
|
272
|
+
self._set_nested_value(default_config, ['azure', 'locations'],
|
|
273
|
+
[loc.strip() for loc in value.split(',') if loc.strip()])
|
|
274
|
+
elif key == 'AZURE_MAX_WORKERS':
|
|
275
|
+
self._set_nested_value(default_config, ['azure', 'max_workers'], int(value))
|
|
276
|
+
else:
|
|
277
|
+
# Generic non-sensitive key handling
|
|
278
|
+
logger.info(f"Unknown non-sensitive key '{key}', storing in default config under 'other'")
|
|
279
|
+
self._set_nested_value(default_config, ['other', key.lower()], value)
|
|
280
|
+
|
|
281
|
+
def _set_nested_value(self, config: Dict[str, Any], path: List[str], value: Any):
|
|
282
|
+
"""Set a nested value in configuration dictionary."""
|
|
283
|
+
current = config
|
|
284
|
+
for key in path[:-1]:
|
|
285
|
+
if key not in current:
|
|
286
|
+
current[key] = {}
|
|
287
|
+
current = current[key]
|
|
288
|
+
current[path[-1]] = value
|
|
289
|
+
|
|
290
|
+
def _get_base_default_config(self) -> Dict[str, Any]:
|
|
291
|
+
"""Get base default configuration structure."""
|
|
292
|
+
return {
|
|
293
|
+
"version": "2.0",
|
|
294
|
+
"logging": {
|
|
295
|
+
"console_level": "ERROR",
|
|
296
|
+
"file_level": "INFO",
|
|
297
|
+
"file_path": "~/.ic/logs/ic_{date}.log",
|
|
298
|
+
"max_files": 30,
|
|
299
|
+
"format": "%(asctime)s [%(levelname)s] - %(message)s",
|
|
300
|
+
"mask_sensitive": True,
|
|
301
|
+
},
|
|
302
|
+
"aws": {
|
|
303
|
+
"config_path": "~/.aws/config",
|
|
304
|
+
"credentials_path": "~/.aws/credentials",
|
|
305
|
+
"accounts": [],
|
|
306
|
+
"regions": ["ap-northeast-2"],
|
|
307
|
+
"cross_account_role": "OrganizationAccountAccessRole",
|
|
308
|
+
"session_duration": 3600,
|
|
309
|
+
"max_workers": 10,
|
|
310
|
+
"tags": {
|
|
311
|
+
"required": ["User", "Team", "Environment"],
|
|
312
|
+
"optional": ["Service", "Application"],
|
|
313
|
+
"rules": {
|
|
314
|
+
"User": "^.+$",
|
|
315
|
+
"Team": "^\\d+$",
|
|
316
|
+
"Environment": "^(PROD|STG|DEV|TEST|QA)$",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
"azure": {
|
|
321
|
+
"subscriptions": [],
|
|
322
|
+
"locations": ["Korea Central"],
|
|
323
|
+
"max_workers": 10,
|
|
324
|
+
},
|
|
325
|
+
"gcp": {
|
|
326
|
+
"mcp": {
|
|
327
|
+
"enabled": True,
|
|
328
|
+
"endpoint": "http://localhost:8080/gcp",
|
|
329
|
+
"auth_method": "service_account",
|
|
330
|
+
"prefer_mcp": True,
|
|
331
|
+
},
|
|
332
|
+
"projects": [],
|
|
333
|
+
"regions": ["asia-northeast3"],
|
|
334
|
+
"zones": ["asia-northeast3-a"],
|
|
335
|
+
"max_workers": 10,
|
|
336
|
+
},
|
|
337
|
+
"oci": {
|
|
338
|
+
"config_path": "~/.oci/config",
|
|
339
|
+
"max_workers": 10,
|
|
340
|
+
},
|
|
341
|
+
"cloudflare": {
|
|
342
|
+
"config_path": "~/.cloudflare/config",
|
|
343
|
+
"accounts": [],
|
|
344
|
+
"zones": [],
|
|
345
|
+
},
|
|
346
|
+
"ssh": {
|
|
347
|
+
"config_file": "~/.ssh/config",
|
|
348
|
+
"key_dir": "~/aws-key",
|
|
349
|
+
"max_workers": 70,
|
|
350
|
+
"skip_prefixes": ["git", "akrr-portx", "akrr-taas-gw", "agw01", "semaphore"],
|
|
351
|
+
"timeouts": {
|
|
352
|
+
"port_scan": 0.5,
|
|
353
|
+
"ssh_connect": 5,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
"mcp": {
|
|
357
|
+
"servers": {
|
|
358
|
+
"github": {
|
|
359
|
+
"enabled": True,
|
|
360
|
+
"auto_approve": [],
|
|
361
|
+
},
|
|
362
|
+
"terraform": {
|
|
363
|
+
"enabled": True,
|
|
364
|
+
"auto_approve": [],
|
|
365
|
+
},
|
|
366
|
+
"aws_docs": {
|
|
367
|
+
"enabled": True,
|
|
368
|
+
"auto_approve": ["read_documentation", "search_documentation"],
|
|
369
|
+
},
|
|
370
|
+
"azure": {
|
|
371
|
+
"enabled": True,
|
|
372
|
+
"auto_approve": ["documentation"],
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
"slack": {
|
|
377
|
+
"enabled": False,
|
|
378
|
+
},
|
|
379
|
+
"security": {
|
|
380
|
+
"sensitive_keys": [
|
|
381
|
+
"password", "passwd", "pwd",
|
|
382
|
+
"token", "access_token", "refresh_token", "auth_token",
|
|
383
|
+
"key", "api_key", "access_key", "secret_key", "private_key",
|
|
384
|
+
"secret", "client_secret", "webhook_secret",
|
|
385
|
+
"webhook_url", "webhook",
|
|
386
|
+
"credential", "credentials",
|
|
387
|
+
"cert", "certificate",
|
|
388
|
+
"session", "session_token",
|
|
389
|
+
],
|
|
390
|
+
"mask_pattern": "***MASKED***",
|
|
391
|
+
"warn_on_sensitive_in_config": True,
|
|
392
|
+
"git_hooks_enabled": True,
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
def _save_yaml_config(self, file_path: Path, config: Dict[str, Any]) -> bool:
|
|
397
|
+
"""
|
|
398
|
+
Save configuration to YAML file.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
file_path: Path to save the file
|
|
402
|
+
config: Configuration dictionary
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
True if successful
|
|
406
|
+
"""
|
|
407
|
+
try:
|
|
408
|
+
import yaml
|
|
409
|
+
|
|
410
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
411
|
+
yaml.dump(config, f, default_flow_style=False, indent=2,
|
|
412
|
+
allow_unicode=True, sort_keys=False)
|
|
413
|
+
|
|
414
|
+
logger.info(f"Saved configuration to {file_path}")
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.error(f"Failed to save configuration to {file_path}: {e}")
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
def _backup_file(self, file_path: Path) -> Optional[Path]:
|
|
422
|
+
"""
|
|
423
|
+
Create a backup of a file.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
file_path: Path to file to backup
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Path to backup file if successful
|
|
430
|
+
"""
|
|
431
|
+
if not file_path.exists():
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
# Create backup directory
|
|
435
|
+
self.backup_dir.mkdir(exist_ok=True)
|
|
436
|
+
|
|
437
|
+
# Generate backup filename with timestamp
|
|
438
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
439
|
+
backup_name = f"{file_path.stem}_{timestamp}{file_path.suffix}"
|
|
440
|
+
backup_path = self.backup_dir / backup_name
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
shutil.copy2(file_path, backup_path)
|
|
444
|
+
logger.info(f"Backed up {file_path} to {backup_path}")
|
|
445
|
+
return backup_path
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Failed to backup {file_path}: {e}")
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
def _record_migration(self, env_path: Path, default_yaml: Path, secrets_yaml: Path):
|
|
451
|
+
"""Record migration details for history."""
|
|
452
|
+
migration_record = {
|
|
453
|
+
"timestamp": datetime.now().isoformat(),
|
|
454
|
+
"source_file": str(env_path),
|
|
455
|
+
"target_files": {
|
|
456
|
+
"default_config": str(default_yaml),
|
|
457
|
+
"secrets_config": str(secrets_yaml) if secrets_yaml.exists() else None
|
|
458
|
+
},
|
|
459
|
+
"backup_location": str(self.backup_dir)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
self.migration_history.append(migration_record)
|
|
463
|
+
|
|
464
|
+
def create_migration_history_document(self) -> bool:
|
|
465
|
+
"""
|
|
466
|
+
Create a migration history document.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
True if document was created successfully
|
|
470
|
+
"""
|
|
471
|
+
try:
|
|
472
|
+
history_content = self._generate_migration_history_content()
|
|
473
|
+
|
|
474
|
+
history_path = self.backup_dir / "migration_history.md"
|
|
475
|
+
with open(history_path, 'w', encoding='utf-8') as f:
|
|
476
|
+
f.write(history_content)
|
|
477
|
+
|
|
478
|
+
logger.info(f"Created migration history document at {history_path}")
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
logger.error(f"Failed to create migration history document: {e}")
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
def _generate_migration_history_content(self) -> str:
|
|
486
|
+
"""Generate migration history document content."""
|
|
487
|
+
content = """# IC Configuration Migration History
|
|
488
|
+
|
|
489
|
+
This document records the migration from .env files to YAML configuration system.
|
|
490
|
+
|
|
491
|
+
## Migration Overview
|
|
492
|
+
|
|
493
|
+
The IC configuration system has been migrated from environment variable-based (.env)
|
|
494
|
+
configuration to a structured YAML-based system with the following benefits:
|
|
495
|
+
|
|
496
|
+
- **Security**: Sensitive data is separated into `config/secrets.yaml`
|
|
497
|
+
- **Structure**: Configuration is organized by service and purpose
|
|
498
|
+
- **Validation**: Built-in validation and security checks
|
|
499
|
+
- **External References**: Direct integration with cloud provider config files
|
|
500
|
+
- **Fixed Logging**: Logs are written to a consistent location
|
|
501
|
+
|
|
502
|
+
## File Structure Changes
|
|
503
|
+
|
|
504
|
+
### Before Migration
|
|
505
|
+
```
|
|
506
|
+
.env # All configuration in one file
|
|
507
|
+
logs/ # Logs created in current directory
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### After Migration
|
|
511
|
+
```
|
|
512
|
+
config/
|
|
513
|
+
├── default.yaml # Non-sensitive configuration
|
|
514
|
+
└── secrets.yaml # Sensitive configuration (600 permissions)
|
|
515
|
+
|
|
516
|
+
~/.ic/
|
|
517
|
+
└── logs/ # Fixed log location
|
|
518
|
+
|
|
519
|
+
backup/
|
|
520
|
+
├── .env_YYYYMMDD_HHMMSS # Backed up original .env
|
|
521
|
+
└── migration_history.md # This document
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## Migration Records
|
|
525
|
+
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
if self.migration_history:
|
|
529
|
+
for i, record in enumerate(self.migration_history, 1):
|
|
530
|
+
content += f"### Migration {i}\n\n"
|
|
531
|
+
content += f"- **Date**: {record['timestamp']}\n"
|
|
532
|
+
content += f"- **Source**: {record['source_file']}\n"
|
|
533
|
+
content += f"- **Default Config**: {record['target_files']['default_config']}\n"
|
|
534
|
+
|
|
535
|
+
if record['target_files']['secrets_config']:
|
|
536
|
+
content += f"- **Secrets Config**: {record['target_files']['secrets_config']}\n"
|
|
537
|
+
|
|
538
|
+
content += f"- **Backup Location**: {record['backup_location']}\n\n"
|
|
539
|
+
else:
|
|
540
|
+
content += "No migration records found.\n\n"
|
|
541
|
+
|
|
542
|
+
content += """## Configuration Categories
|
|
543
|
+
|
|
544
|
+
### Default Configuration (config/default.yaml)
|
|
545
|
+
- Logging settings
|
|
546
|
+
- Service endpoints and regions
|
|
547
|
+
- Worker thread counts
|
|
548
|
+
- Timeout values
|
|
549
|
+
- Tag validation rules
|
|
550
|
+
- External config file paths
|
|
551
|
+
|
|
552
|
+
### Secrets Configuration (config/secrets.yaml)
|
|
553
|
+
- API tokens and keys
|
|
554
|
+
- Account IDs and credentials
|
|
555
|
+
- Webhook URLs
|
|
556
|
+
- Service account paths
|
|
557
|
+
- Subscription IDs
|
|
558
|
+
|
|
559
|
+
## Security Notes
|
|
560
|
+
|
|
561
|
+
1. **File Permissions**: `config/secrets.yaml` should have 600 permissions (owner read/write only)
|
|
562
|
+
2. **Version Control**: Add `config/secrets.yaml` to `.gitignore`
|
|
563
|
+
3. **Environment Fallback**: If secrets.yaml is missing, system falls back to environment variables
|
|
564
|
+
4. **Sensitive Data Masking**: All logs automatically mask sensitive information
|
|
565
|
+
|
|
566
|
+
## Rollback Instructions
|
|
567
|
+
|
|
568
|
+
If you need to rollback to the .env system:
|
|
569
|
+
|
|
570
|
+
1. Copy the backed up .env file from the backup directory
|
|
571
|
+
2. Remove or rename the config/ directory
|
|
572
|
+
3. Restart the application
|
|
573
|
+
|
|
574
|
+
## Next Steps
|
|
575
|
+
|
|
576
|
+
1. Review the generated configuration files
|
|
577
|
+
2. Update any service-specific settings as needed
|
|
578
|
+
3. Ensure secrets.yaml has proper file permissions
|
|
579
|
+
4. Add secrets.yaml to .gitignore if using version control
|
|
580
|
+
5. Test all services to ensure proper configuration loading
|
|
581
|
+
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
return content
|
|
585
|
+
|
|
586
|
+
def validate_migration(self) -> Dict[str, List[str]]:
|
|
587
|
+
"""
|
|
588
|
+
Validate the migration results.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Dictionary of validation results
|
|
592
|
+
"""
|
|
593
|
+
issues = {
|
|
594
|
+
"errors": [],
|
|
595
|
+
"warnings": [],
|
|
596
|
+
"info": []
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# Check if config files exist
|
|
600
|
+
config_dir = Path("config")
|
|
601
|
+
default_yaml = config_dir / "default.yaml"
|
|
602
|
+
secrets_yaml = config_dir / "secrets.yaml"
|
|
603
|
+
|
|
604
|
+
if not default_yaml.exists():
|
|
605
|
+
issues["errors"].append("default.yaml not found in config directory")
|
|
606
|
+
|
|
607
|
+
if not secrets_yaml.exists():
|
|
608
|
+
issues["warnings"].append("secrets.yaml not found - will use environment variables")
|
|
609
|
+
else:
|
|
610
|
+
# Check file permissions
|
|
611
|
+
try:
|
|
612
|
+
file_mode = secrets_yaml.stat().st_mode & 0o777
|
|
613
|
+
if file_mode != 0o600:
|
|
614
|
+
issues["warnings"].append(f"secrets.yaml has insecure permissions: {oct(file_mode)}")
|
|
615
|
+
except Exception as e:
|
|
616
|
+
issues["warnings"].append(f"Could not check secrets.yaml permissions: {e}")
|
|
617
|
+
|
|
618
|
+
# Check if backup was created
|
|
619
|
+
if not self.backup_dir.exists():
|
|
620
|
+
issues["warnings"].append("No backup directory found")
|
|
621
|
+
else:
|
|
622
|
+
backup_files = list(self.backup_dir.glob("*.env*"))
|
|
623
|
+
if not backup_files:
|
|
624
|
+
issues["warnings"].append("No .env backup files found")
|
|
625
|
+
else:
|
|
626
|
+
issues["info"].append(f"Found {len(backup_files)} backup files")
|
|
627
|
+
|
|
628
|
+
return issues
|