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/schema.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration schema validation module for IC.
|
|
3
|
+
|
|
4
|
+
This module provides data models and validation for IC configuration files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Dict, List, Any, Optional, Union
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LoggingConfig:
|
|
18
|
+
"""Logging configuration data model."""
|
|
19
|
+
console_level: str = "ERROR"
|
|
20
|
+
file_level: str = "INFO"
|
|
21
|
+
file_path: str = "logs/ic_{date}.log"
|
|
22
|
+
max_files: int = 30
|
|
23
|
+
format: str = "%(asctime)s [%(levelname)s] - %(message)s"
|
|
24
|
+
mask_sensitive: bool = True
|
|
25
|
+
|
|
26
|
+
def validate(self) -> List[str]:
|
|
27
|
+
"""Validate logging configuration."""
|
|
28
|
+
errors = []
|
|
29
|
+
|
|
30
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
31
|
+
if self.console_level not in valid_levels:
|
|
32
|
+
errors.append(f"Invalid console_level: {self.console_level}. Must be one of {valid_levels}")
|
|
33
|
+
|
|
34
|
+
if self.file_level not in valid_levels:
|
|
35
|
+
errors.append(f"Invalid file_level: {self.file_level}. Must be one of {valid_levels}")
|
|
36
|
+
|
|
37
|
+
if self.max_files < 1:
|
|
38
|
+
errors.append("max_files must be at least 1")
|
|
39
|
+
|
|
40
|
+
if not self.file_path:
|
|
41
|
+
errors.append("file_path cannot be empty")
|
|
42
|
+
|
|
43
|
+
return errors
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TagConfig:
|
|
48
|
+
"""Tag validation configuration."""
|
|
49
|
+
required: List[str] = field(default_factory=lambda: ["User", "Team", "Environment"])
|
|
50
|
+
optional: List[str] = field(default_factory=lambda: ["Service", "Application"])
|
|
51
|
+
rules: Dict[str, str] = field(default_factory=lambda: {
|
|
52
|
+
"User": "^.+$",
|
|
53
|
+
"Team": "^\\d+$",
|
|
54
|
+
"Environment": "^(PROD|STG|DEV|TEST|QA)$",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
def validate(self) -> List[str]:
|
|
58
|
+
"""Validate tag configuration."""
|
|
59
|
+
errors = []
|
|
60
|
+
|
|
61
|
+
# Validate regex patterns
|
|
62
|
+
for tag_name, pattern in self.rules.items():
|
|
63
|
+
try:
|
|
64
|
+
re.compile(pattern)
|
|
65
|
+
except re.error as e:
|
|
66
|
+
errors.append(f"Invalid regex pattern for tag '{tag_name}': {e}")
|
|
67
|
+
|
|
68
|
+
return errors
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class AWSConfig:
|
|
73
|
+
"""AWS configuration data model."""
|
|
74
|
+
accounts: List[str] = field(default_factory=list)
|
|
75
|
+
regions: List[str] = field(default_factory=lambda: ["ap-northeast-2"])
|
|
76
|
+
cross_account_role: str = "OrganizationAccountAccessRole"
|
|
77
|
+
session_duration: int = 3600
|
|
78
|
+
max_workers: int = 10
|
|
79
|
+
tags: TagConfig = field(default_factory=TagConfig)
|
|
80
|
+
default_profile: Optional[str] = None
|
|
81
|
+
default_region: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
def validate(self) -> List[str]:
|
|
84
|
+
"""Validate AWS configuration."""
|
|
85
|
+
errors = []
|
|
86
|
+
|
|
87
|
+
# Validate account IDs
|
|
88
|
+
for account_id in self.accounts:
|
|
89
|
+
if not isinstance(account_id, str) or not re.match(r'^\d{12}$', account_id):
|
|
90
|
+
errors.append(f"Invalid AWS account ID: {account_id}. Must be 12 digits")
|
|
91
|
+
|
|
92
|
+
# Validate regions
|
|
93
|
+
aws_regions = [
|
|
94
|
+
"us-east-1", "us-east-2", "us-west-1", "us-west-2",
|
|
95
|
+
"ap-northeast-1", "ap-northeast-2", "ap-northeast-3",
|
|
96
|
+
"ap-southeast-1", "ap-southeast-2", "ap-south-1",
|
|
97
|
+
"eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1",
|
|
98
|
+
"ca-central-1", "sa-east-1"
|
|
99
|
+
]
|
|
100
|
+
for region in self.regions:
|
|
101
|
+
if region not in aws_regions:
|
|
102
|
+
errors.append(f"Unknown AWS region: {region}")
|
|
103
|
+
|
|
104
|
+
# Validate session duration
|
|
105
|
+
if not (900 <= self.session_duration <= 43200): # 15 minutes to 12 hours
|
|
106
|
+
errors.append("session_duration must be between 900 and 43200 seconds")
|
|
107
|
+
|
|
108
|
+
# Validate max_workers
|
|
109
|
+
if self.max_workers < 1:
|
|
110
|
+
errors.append("max_workers must be at least 1")
|
|
111
|
+
|
|
112
|
+
# Validate tags
|
|
113
|
+
errors.extend(self.tags.validate())
|
|
114
|
+
|
|
115
|
+
return errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class AzureConfig:
|
|
120
|
+
"""Azure configuration data model."""
|
|
121
|
+
subscriptions: List[str] = field(default_factory=list)
|
|
122
|
+
locations: List[str] = field(default_factory=lambda: ["Korea Central"])
|
|
123
|
+
max_workers: int = 10
|
|
124
|
+
tenant_id: Optional[str] = None
|
|
125
|
+
client_id: Optional[str] = None
|
|
126
|
+
client_secret: Optional[str] = None
|
|
127
|
+
subscription_id: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
def validate(self) -> List[str]:
|
|
130
|
+
"""Validate Azure configuration."""
|
|
131
|
+
errors = []
|
|
132
|
+
|
|
133
|
+
# Validate subscription IDs (UUIDs)
|
|
134
|
+
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
135
|
+
for sub_id in self.subscriptions:
|
|
136
|
+
if not re.match(uuid_pattern, sub_id, re.IGNORECASE):
|
|
137
|
+
errors.append(f"Invalid Azure subscription ID format: {sub_id}")
|
|
138
|
+
|
|
139
|
+
# Validate max_workers
|
|
140
|
+
if self.max_workers < 1:
|
|
141
|
+
errors.append("max_workers must be at least 1")
|
|
142
|
+
|
|
143
|
+
# Validate tenant_id if provided
|
|
144
|
+
if self.tenant_id and not re.match(uuid_pattern, self.tenant_id, re.IGNORECASE):
|
|
145
|
+
errors.append(f"Invalid Azure tenant ID format: {self.tenant_id}")
|
|
146
|
+
|
|
147
|
+
# Validate client_id if provided
|
|
148
|
+
if self.client_id and not re.match(uuid_pattern, self.client_id, re.IGNORECASE):
|
|
149
|
+
errors.append(f"Invalid Azure client ID format: {self.client_id}")
|
|
150
|
+
|
|
151
|
+
return errors
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class GCPMCPConfig:
|
|
156
|
+
"""GCP MCP configuration."""
|
|
157
|
+
enabled: bool = True
|
|
158
|
+
endpoint: str = "http://localhost:8080/gcp"
|
|
159
|
+
auth_method: str = "service_account"
|
|
160
|
+
prefer_mcp: bool = True
|
|
161
|
+
|
|
162
|
+
def validate(self) -> List[str]:
|
|
163
|
+
"""Validate GCP MCP configuration."""
|
|
164
|
+
errors = []
|
|
165
|
+
|
|
166
|
+
valid_auth_methods = ["service_account", "oauth", "default"]
|
|
167
|
+
if self.auth_method not in valid_auth_methods:
|
|
168
|
+
errors.append(f"Invalid auth_method: {self.auth_method}. Must be one of {valid_auth_methods}")
|
|
169
|
+
|
|
170
|
+
return errors
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class GCPConfig:
|
|
175
|
+
"""GCP configuration data model."""
|
|
176
|
+
mcp: GCPMCPConfig = field(default_factory=GCPMCPConfig)
|
|
177
|
+
projects: List[str] = field(default_factory=list)
|
|
178
|
+
regions: List[str] = field(default_factory=lambda: ["asia-northeast3"])
|
|
179
|
+
zones: List[str] = field(default_factory=lambda: ["asia-northeast3-a"])
|
|
180
|
+
max_workers: int = 10
|
|
181
|
+
service_account_key_path: Optional[str] = None
|
|
182
|
+
project_id: Optional[str] = None
|
|
183
|
+
|
|
184
|
+
def validate(self) -> List[str]:
|
|
185
|
+
"""Validate GCP configuration."""
|
|
186
|
+
errors = []
|
|
187
|
+
|
|
188
|
+
# Validate project IDs
|
|
189
|
+
project_pattern = r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$'
|
|
190
|
+
for project_id in self.projects:
|
|
191
|
+
if not re.match(project_pattern, project_id):
|
|
192
|
+
errors.append(f"Invalid GCP project ID: {project_id}")
|
|
193
|
+
|
|
194
|
+
# Validate max_workers
|
|
195
|
+
if self.max_workers < 1:
|
|
196
|
+
errors.append("max_workers must be at least 1")
|
|
197
|
+
|
|
198
|
+
# Validate service account key path if provided
|
|
199
|
+
if self.service_account_key_path:
|
|
200
|
+
key_path = Path(self.service_account_key_path).expanduser()
|
|
201
|
+
if not key_path.exists():
|
|
202
|
+
errors.append(f"Service account key file not found: {self.service_account_key_path}")
|
|
203
|
+
|
|
204
|
+
# Validate MCP configuration
|
|
205
|
+
errors.extend(self.mcp.validate())
|
|
206
|
+
|
|
207
|
+
return errors
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass
|
|
211
|
+
class OCIConfig:
|
|
212
|
+
"""OCI configuration data model."""
|
|
213
|
+
config_path: str = "~/.oci/config"
|
|
214
|
+
max_workers: int = 10
|
|
215
|
+
|
|
216
|
+
def validate(self) -> List[str]:
|
|
217
|
+
"""Validate OCI configuration."""
|
|
218
|
+
errors = []
|
|
219
|
+
|
|
220
|
+
if self.max_workers < 1:
|
|
221
|
+
errors.append("max_workers must be at least 1")
|
|
222
|
+
|
|
223
|
+
# Check if config file exists
|
|
224
|
+
config_path = Path(self.config_path).expanduser()
|
|
225
|
+
if not config_path.exists():
|
|
226
|
+
errors.append(f"OCI config file not found: {self.config_path}")
|
|
227
|
+
|
|
228
|
+
return errors
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class CloudFlareConfig:
|
|
233
|
+
"""CloudFlare configuration data model."""
|
|
234
|
+
accounts: List[str] = field(default_factory=list)
|
|
235
|
+
zones: List[str] = field(default_factory=list)
|
|
236
|
+
email: Optional[str] = None
|
|
237
|
+
api_token: Optional[str] = None
|
|
238
|
+
|
|
239
|
+
def validate(self) -> List[str]:
|
|
240
|
+
"""Validate CloudFlare configuration."""
|
|
241
|
+
errors = []
|
|
242
|
+
|
|
243
|
+
# Validate email format if provided
|
|
244
|
+
if self.email:
|
|
245
|
+
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
246
|
+
if not re.match(email_pattern, self.email):
|
|
247
|
+
errors.append(f"Invalid email format: {self.email}")
|
|
248
|
+
|
|
249
|
+
return errors
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class SSHTimeoutConfig:
|
|
254
|
+
"""SSH timeout configuration."""
|
|
255
|
+
port_scan: float = 0.5
|
|
256
|
+
ssh_connect: int = 5
|
|
257
|
+
|
|
258
|
+
def validate(self) -> List[str]:
|
|
259
|
+
"""Validate SSH timeout configuration."""
|
|
260
|
+
errors = []
|
|
261
|
+
|
|
262
|
+
if self.port_scan <= 0:
|
|
263
|
+
errors.append("port_scan timeout must be positive")
|
|
264
|
+
|
|
265
|
+
if self.ssh_connect <= 0:
|
|
266
|
+
errors.append("ssh_connect timeout must be positive")
|
|
267
|
+
|
|
268
|
+
return errors
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class SSHConfig:
|
|
273
|
+
"""SSH configuration data model."""
|
|
274
|
+
config_file: str = "~/.ssh/config"
|
|
275
|
+
key_dir: str = "~/aws-key"
|
|
276
|
+
max_workers: int = 70
|
|
277
|
+
timeouts: SSHTimeoutConfig = field(default_factory=SSHTimeoutConfig)
|
|
278
|
+
|
|
279
|
+
def validate(self) -> List[str]:
|
|
280
|
+
"""Validate SSH configuration."""
|
|
281
|
+
errors = []
|
|
282
|
+
|
|
283
|
+
if self.max_workers < 1:
|
|
284
|
+
errors.append("max_workers must be at least 1")
|
|
285
|
+
|
|
286
|
+
# Validate timeouts
|
|
287
|
+
errors.extend(self.timeouts.validate())
|
|
288
|
+
|
|
289
|
+
return errors
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class MCPServerConfig:
|
|
294
|
+
"""MCP server configuration."""
|
|
295
|
+
enabled: bool = True
|
|
296
|
+
auto_approve: List[str] = field(default_factory=list)
|
|
297
|
+
personal_access_token: Optional[str] = None
|
|
298
|
+
|
|
299
|
+
def validate(self) -> List[str]:
|
|
300
|
+
"""Validate MCP server configuration."""
|
|
301
|
+
return [] # Basic validation, can be extended
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class MCPConfig:
|
|
306
|
+
"""MCP configuration data model."""
|
|
307
|
+
servers: Dict[str, MCPServerConfig] = field(default_factory=lambda: {
|
|
308
|
+
"github": MCPServerConfig(),
|
|
309
|
+
"terraform": MCPServerConfig(),
|
|
310
|
+
"aws_docs": MCPServerConfig(auto_approve=["read_documentation", "search_documentation"]),
|
|
311
|
+
"azure": MCPServerConfig(auto_approve=["documentation"]),
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
def validate(self) -> List[str]:
|
|
315
|
+
"""Validate MCP configuration."""
|
|
316
|
+
errors = []
|
|
317
|
+
|
|
318
|
+
for server_name, server_config in self.servers.items():
|
|
319
|
+
server_errors = server_config.validate()
|
|
320
|
+
errors.extend([f"MCP server '{server_name}': {error}" for error in server_errors])
|
|
321
|
+
|
|
322
|
+
return errors
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class SlackConfig:
|
|
327
|
+
"""Slack configuration data model."""
|
|
328
|
+
enabled: bool = False
|
|
329
|
+
webhook_url: Optional[str] = None
|
|
330
|
+
|
|
331
|
+
def validate(self) -> List[str]:
|
|
332
|
+
"""Validate Slack configuration."""
|
|
333
|
+
errors = []
|
|
334
|
+
|
|
335
|
+
if self.enabled and not self.webhook_url:
|
|
336
|
+
errors.append("webhook_url is required when Slack is enabled")
|
|
337
|
+
|
|
338
|
+
if self.webhook_url and not self.webhook_url.startswith('https://hooks.slack.com/'):
|
|
339
|
+
errors.append("Invalid Slack webhook URL format")
|
|
340
|
+
|
|
341
|
+
return errors
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass
|
|
345
|
+
class SecurityConfig:
|
|
346
|
+
"""Security configuration data model."""
|
|
347
|
+
sensitive_keys: List[str] = field(default_factory=lambda: [
|
|
348
|
+
"password", "passwd", "pwd",
|
|
349
|
+
"token", "access_token", "refresh_token", "auth_token",
|
|
350
|
+
"key", "api_key", "access_key", "secret_key", "private_key",
|
|
351
|
+
"secret", "client_secret", "webhook_secret",
|
|
352
|
+
"webhook_url", "webhook",
|
|
353
|
+
"credential", "credentials",
|
|
354
|
+
"cert", "certificate",
|
|
355
|
+
"session", "session_token",
|
|
356
|
+
])
|
|
357
|
+
mask_pattern: str = "***MASKED***"
|
|
358
|
+
warn_on_sensitive_in_config: bool = True
|
|
359
|
+
git_hooks_enabled: bool = True
|
|
360
|
+
|
|
361
|
+
def validate(self) -> List[str]:
|
|
362
|
+
"""Validate security configuration."""
|
|
363
|
+
errors = []
|
|
364
|
+
|
|
365
|
+
if not self.sensitive_keys:
|
|
366
|
+
errors.append("sensitive_keys cannot be empty")
|
|
367
|
+
|
|
368
|
+
if not self.mask_pattern:
|
|
369
|
+
errors.append("mask_pattern cannot be empty")
|
|
370
|
+
|
|
371
|
+
return errors
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass
|
|
375
|
+
class ICConfig:
|
|
376
|
+
"""Main IC configuration data model."""
|
|
377
|
+
version: str = "1.0"
|
|
378
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
379
|
+
aws: AWSConfig = field(default_factory=AWSConfig)
|
|
380
|
+
azure: AzureConfig = field(default_factory=AzureConfig)
|
|
381
|
+
gcp: GCPConfig = field(default_factory=GCPConfig)
|
|
382
|
+
oci: OCIConfig = field(default_factory=OCIConfig)
|
|
383
|
+
cloudflare: CloudFlareConfig = field(default_factory=CloudFlareConfig)
|
|
384
|
+
ssh: SSHConfig = field(default_factory=SSHConfig)
|
|
385
|
+
mcp: MCPConfig = field(default_factory=MCPConfig)
|
|
386
|
+
slack: SlackConfig = field(default_factory=SlackConfig)
|
|
387
|
+
security: SecurityConfig = field(default_factory=SecurityConfig)
|
|
388
|
+
|
|
389
|
+
def validate(self) -> List[str]:
|
|
390
|
+
"""Validate entire configuration."""
|
|
391
|
+
errors = []
|
|
392
|
+
|
|
393
|
+
# Validate version
|
|
394
|
+
if not self.version:
|
|
395
|
+
errors.append("version cannot be empty")
|
|
396
|
+
|
|
397
|
+
# Validate each section
|
|
398
|
+
errors.extend([f"logging: {error}" for error in self.logging.validate()])
|
|
399
|
+
errors.extend([f"aws: {error}" for error in self.aws.validate()])
|
|
400
|
+
errors.extend([f"azure: {error}" for error in self.azure.validate()])
|
|
401
|
+
errors.extend([f"gcp: {error}" for error in self.gcp.validate()])
|
|
402
|
+
errors.extend([f"oci: {error}" for error in self.oci.validate()])
|
|
403
|
+
errors.extend([f"cloudflare: {error}" for error in self.cloudflare.validate()])
|
|
404
|
+
errors.extend([f"ssh: {error}" for error in self.ssh.validate()])
|
|
405
|
+
errors.extend([f"mcp: {error}" for error in self.mcp.validate()])
|
|
406
|
+
errors.extend([f"slack: {error}" for error in self.slack.validate()])
|
|
407
|
+
errors.extend([f"security: {error}" for error in self.security.validate()])
|
|
408
|
+
|
|
409
|
+
return errors
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class ConfigValidator:
|
|
413
|
+
"""Configuration validator with comprehensive error reporting."""
|
|
414
|
+
|
|
415
|
+
def __init__(self):
|
|
416
|
+
"""Initialize ConfigValidator."""
|
|
417
|
+
self.errors: List[str] = []
|
|
418
|
+
self.warnings: List[str] = []
|
|
419
|
+
|
|
420
|
+
def validate_config_dict(self, config_data: Dict[str, Any]) -> Dict[str, List[str]]:
|
|
421
|
+
"""
|
|
422
|
+
Validate configuration dictionary and return detailed results.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
config_data: Configuration dictionary to validate
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Dictionary with 'errors' and 'warnings' lists
|
|
429
|
+
"""
|
|
430
|
+
self.errors = []
|
|
431
|
+
self.warnings = []
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# Convert dict to dataclass for validation
|
|
435
|
+
ic_config = self._dict_to_dataclass(config_data)
|
|
436
|
+
|
|
437
|
+
# Validate the configuration
|
|
438
|
+
validation_errors = ic_config.validate()
|
|
439
|
+
self.errors.extend(validation_errors)
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
self.errors.append(f"Configuration structure error: {e}")
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
'errors': self.errors,
|
|
446
|
+
'warnings': self.warnings
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
def _dict_to_dataclass(self, config_data: Dict[str, Any]) -> ICConfig:
|
|
450
|
+
"""
|
|
451
|
+
Convert configuration dictionary to ICConfig dataclass.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
config_data: Configuration dictionary
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
ICConfig instance
|
|
458
|
+
"""
|
|
459
|
+
# This is a simplified conversion - in a real implementation,
|
|
460
|
+
# you might want to use a library like dacite or cattrs
|
|
461
|
+
|
|
462
|
+
# Extract and convert each section
|
|
463
|
+
logging_data = config_data.get('logging', {})
|
|
464
|
+
logging_config = LoggingConfig(
|
|
465
|
+
console_level=logging_data.get('console_level', 'ERROR'),
|
|
466
|
+
file_level=logging_data.get('file_level', 'INFO'),
|
|
467
|
+
file_path=logging_data.get('file_path', 'logs/ic_{date}.log'),
|
|
468
|
+
max_files=logging_data.get('max_files', 30),
|
|
469
|
+
format=logging_data.get('format', '%(asctime)s [%(levelname)s] - %(message)s'),
|
|
470
|
+
mask_sensitive=logging_data.get('mask_sensitive', True),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# AWS configuration
|
|
474
|
+
aws_data = config_data.get('aws', {})
|
|
475
|
+
tags_data = aws_data.get('tags', {})
|
|
476
|
+
tag_config = TagConfig(
|
|
477
|
+
required=tags_data.get('required', ["User", "Team", "Environment"]),
|
|
478
|
+
optional=tags_data.get('optional', ["Service", "Application"]),
|
|
479
|
+
rules=tags_data.get('rules', {
|
|
480
|
+
"User": "^.+$",
|
|
481
|
+
"Team": "^\\d+$",
|
|
482
|
+
"Environment": "^(PROD|STG|DEV|TEST|QA)$",
|
|
483
|
+
}),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
aws_config = AWSConfig(
|
|
487
|
+
accounts=aws_data.get('accounts', []),
|
|
488
|
+
regions=aws_data.get('regions', ["ap-northeast-2"]),
|
|
489
|
+
cross_account_role=aws_data.get('cross_account_role', 'OrganizationAccountAccessRole'),
|
|
490
|
+
session_duration=aws_data.get('session_duration', 3600),
|
|
491
|
+
max_workers=aws_data.get('max_workers', 10),
|
|
492
|
+
tags=tag_config,
|
|
493
|
+
default_profile=aws_data.get('default_profile'),
|
|
494
|
+
default_region=aws_data.get('default_region'),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Continue with other sections...
|
|
498
|
+
# For brevity, I'll create a basic version
|
|
499
|
+
|
|
500
|
+
return ICConfig(
|
|
501
|
+
version=config_data.get('version', '1.0'),
|
|
502
|
+
logging=logging_config,
|
|
503
|
+
aws=aws_config,
|
|
504
|
+
# Add other sections as needed
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def validate_json_schema(self, config_data: Dict[str, Any]) -> Dict[str, List[str]]:
|
|
508
|
+
"""
|
|
509
|
+
Validate configuration against JSON schema.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
config_data: Configuration data to validate
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Dictionary with validation results
|
|
516
|
+
"""
|
|
517
|
+
# This would use jsonschema library for validation
|
|
518
|
+
# For now, return basic validation
|
|
519
|
+
return self.validate_config_dict(config_data)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def get_json_schema() -> Dict[str, Any]:
|
|
523
|
+
"""
|
|
524
|
+
Get JSON schema for IC configuration.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
JSON schema dictionary
|
|
528
|
+
"""
|
|
529
|
+
return {
|
|
530
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
531
|
+
"title": "IC Configuration Schema",
|
|
532
|
+
"type": "object",
|
|
533
|
+
"required": ["version"],
|
|
534
|
+
"properties": {
|
|
535
|
+
"version": {
|
|
536
|
+
"type": "string",
|
|
537
|
+
"pattern": "^\\d+\\.\\d+$"
|
|
538
|
+
},
|
|
539
|
+
"logging": {
|
|
540
|
+
"type": "object",
|
|
541
|
+
"properties": {
|
|
542
|
+
"console_level": {
|
|
543
|
+
"type": "string",
|
|
544
|
+
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
545
|
+
},
|
|
546
|
+
"file_level": {
|
|
547
|
+
"type": "string",
|
|
548
|
+
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
549
|
+
},
|
|
550
|
+
"file_path": {"type": "string"},
|
|
551
|
+
"max_files": {"type": "integer", "minimum": 1},
|
|
552
|
+
"format": {"type": "string"},
|
|
553
|
+
"mask_sensitive": {"type": "boolean"}
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
"aws": {
|
|
557
|
+
"type": "object",
|
|
558
|
+
"properties": {
|
|
559
|
+
"accounts": {
|
|
560
|
+
"type": "array",
|
|
561
|
+
"items": {
|
|
562
|
+
"type": "string",
|
|
563
|
+
"pattern": "^\\d{12}$"
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
"regions": {
|
|
567
|
+
"type": "array",
|
|
568
|
+
"items": {"type": "string"}
|
|
569
|
+
},
|
|
570
|
+
"cross_account_role": {"type": "string"},
|
|
571
|
+
"session_duration": {
|
|
572
|
+
"type": "integer",
|
|
573
|
+
"minimum": 900,
|
|
574
|
+
"maximum": 43200
|
|
575
|
+
},
|
|
576
|
+
"max_workers": {
|
|
577
|
+
"type": "integer",
|
|
578
|
+
"minimum": 1
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
"security": {
|
|
583
|
+
"type": "object",
|
|
584
|
+
"properties": {
|
|
585
|
+
"sensitive_keys": {
|
|
586
|
+
"type": "array",
|
|
587
|
+
"items": {"type": "string"}
|
|
588
|
+
},
|
|
589
|
+
"mask_pattern": {"type": "string"},
|
|
590
|
+
"warn_on_sensitive_in_config": {"type": "boolean"},
|
|
591
|
+
"git_hooks_enabled": {"type": "boolean"}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|