UNPKG

auto-publishing-mcp-server

Version:

Enterprise-grade MCP Server for Auto-Publishing with pre-publish validation, multi-cloud deployment, and monitoring

476 lines (407 loc) 14.6 kB
/** * 3대 핵심 요소 검증 시스템 * Docker ID, Domain, Internal IP 검증 로직 */ import { promises as dns } from 'dns'; import { execSync } from 'child_process'; import { ErrorCreators } from '../../utils/errors.js'; export class DeploymentValidator { constructor() { // IP 대역 설정 this.IP_RANGES = { WORDPRESS: '192.168.100.0/24', WEBAPP: '192.168.101.0/24', MICROSERVICE: '192.168.102.0/24' // 향후 확장용 }; // Docker ID 규칙 this.DOCKER_ID_RULES = { MIN_LENGTH: 3, MAX_LENGTH: 63, PATTERN: /^[a-z0-9][a-z0-9-]*[a-z0-9]$/, FORBIDDEN_PATTERNS: [ /^docker-/, // docker- 접두사 금지 /^[0-9]+$/, // 숫자만 구성 금지 /-{2,}/, // 연속 하이픈 금지 /^test-.*$/ // test- 접두사 제한 ] }; // Domain 규칙 this.DOMAIN_RULES = { PATTERN: /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/, MAX_LENGTH: 253, RESERVED_DOMAINS: [ 'localhost', 'example.com', 'test.com', 'internal' ] }; // 사용 중인 리소스 캐시 this.usedResources = { dockerIds: new Set(), domains: new Set(), ips: new Set() }; this.lastCacheUpdate = null; this.CACHE_TTL = 60000; // 1분 } /** * 전체 배포 사전 검증 */ async validateDeployment(args) { const { dockerId, domain, internalIp, environment = 'dev' } = args; if (!dockerId || !domain || !internalIp) { throw new Error('dockerId, domain, internalIp are required for validation'); } try { console.log(`🔍 Starting deployment validation for ${dockerId}`); // 캐시 업데이트 await this.updateResourceCache(); const validationResults = { dockerId: await this.validateDockerId(dockerId), domain: await this.validateDomain(domain), internalIp: await this.validateInternalIp(internalIp, environment), networkConnectivity: await this.validateNetworkConnectivity(internalIp), resourceAvailability: await this.validateResourceAvailability(dockerId, domain, internalIp) }; // 전체 검증 결과 종합 const allValid = Object.values(validationResults).every(result => result.valid); const issues = Object.values(validationResults) .filter(result => !result.valid) .flatMap(result => result.issues || []); return { output: allValid ? `✅ Deployment validation passed for ${dockerId}` : `❌ Deployment validation failed for ${dockerId}`, data: { valid: allValid, environment: environment, validationResults: validationResults, issues: issues, recommendations: allValid ? [] : this.generateRecommendations(validationResults), timestamp: new Date().toISOString() } }; } catch (error) { throw ErrorCreators.toolExecutionError('deploy/validate', error); } } /** * Docker ID 네이밍 규칙 검증 */ async validateDockerId(dockerId) { const issues = []; // 길이 검증 if (dockerId.length < this.DOCKER_ID_RULES.MIN_LENGTH) { issues.push(`Docker ID must be at least ${this.DOCKER_ID_RULES.MIN_LENGTH} characters long`); } if (dockerId.length > this.DOCKER_ID_RULES.MAX_LENGTH) { issues.push(`Docker ID must be no more than ${this.DOCKER_ID_RULES.MAX_LENGTH} characters long`); } // 패턴 검증 if (!this.DOCKER_ID_RULES.PATTERN.test(dockerId)) { issues.push('Docker ID must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen'); } // 금지된 패턴 검증 for (const forbiddenPattern of this.DOCKER_ID_RULES.FORBIDDEN_PATTERNS) { if (forbiddenPattern.test(dockerId)) { issues.push(`Docker ID matches forbidden pattern: ${forbiddenPattern.source}`); } } // 중복 검증 if (this.usedResources.dockerIds.has(dockerId)) { issues.push(`Docker ID '${dockerId}' is already in use`); } // 실제 Docker 컨테이너 중복 검증 try { const runningContainers = execSync('docker ps --format "{{.Names}}"', { encoding: 'utf8' }); if (runningContainers.includes(dockerId)) { issues.push(`Docker container with name '${dockerId}' is already running`); } } catch (error) { console.warn('Could not check running Docker containers:', error.message); } return { valid: issues.length === 0, dockerId: dockerId, issues: issues, rules: { length: `${this.DOCKER_ID_RULES.MIN_LENGTH}-${this.DOCKER_ID_RULES.MAX_LENGTH} characters`, pattern: 'lowercase letters, numbers, hyphens only', restrictions: 'no docker- prefix, no consecutive hyphens' } }; } /** * Domain 형식 및 가용성 검증 */ async validateDomain(domain) { const issues = []; // 기본 형식 검증 if (!this.DOMAIN_RULES.PATTERN.test(domain.toLowerCase())) { issues.push('Domain format is invalid'); } if (domain.length > this.DOMAIN_RULES.MAX_LENGTH) { issues.push(`Domain must be no more than ${this.DOMAIN_RULES.MAX_LENGTH} characters long`); } // 예약된 도메인 검증 const lowerDomain = domain.toLowerCase(); if (this.DOMAIN_RULES.RESERVED_DOMAINS.includes(lowerDomain)) { issues.push(`Domain '${domain}' is reserved and cannot be used`); } // 중복 검증 if (this.usedResources.domains.has(lowerDomain)) { issues.push(`Domain '${domain}' is already in use`); } // DNS 해결 가능성 검증 (외부 도메인인 경우) let dnsResolvable = false; if (domain.includes('.') && !domain.endsWith('.local')) { try { await dns.resolve(domain, 'A'); dnsResolvable = true; issues.push(`Domain '${domain}' already exists in public DNS`); } catch (error) { // DNS 해결 불가능 - 새 도메인으로 사용 가능 dnsResolvable = false; } } return { valid: issues.length === 0, domain: domain, dnsResolvable: dnsResolvable, issues: issues, rules: { format: 'valid domain format (a-z, 0-9, hyphen, dot)', length: `max ${this.DOMAIN_RULES.MAX_LENGTH} characters`, restrictions: 'no reserved domains' } }; } /** * Internal IP 충돌 방지 및 가용성 검증 */ async validateInternalIp(internalIp, environment) { const issues = []; // IP 형식 검증 const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const match = internalIp.match(ipPattern); if (!match) { issues.push('Invalid IP address format'); return { valid: false, internalIp, issues }; } const [, oct1, oct2, oct3, oct4] = match.map(Number); // IP 범위 검증 if (oct1 < 0 || oct1 > 255 || oct2 < 0 || oct2 > 255 || oct3 < 0 || oct3 > 255 || oct4 < 0 || oct4 > 255) { issues.push('IP address octets must be between 0-255'); } // 내부 대역 검증 const isValidInternalRange = (oct1 === 192 && oct2 === 168 && (oct3 === 100 || oct3 === 101 || oct3 === 102)) || (oct1 === 10) || (oct1 === 172 && oct2 >= 16 && oct2 <= 31); if (!isValidInternalRange) { issues.push('IP must be in private address range (192.168.100.x, 192.168.101.x, 10.x.x.x, or 172.16-31.x.x)'); } // 환경별 대역 검증 const environmentRanges = { 'dev': [100, 101], // 192.168.100.x, 192.168.101.x 'staging': [101], // 192.168.101.x 'prod': [101, 102] // 192.168.101.x, 192.168.102.x }; if (oct1 === 192 && oct2 === 168 && environmentRanges[environment]) { if (!environmentRanges[environment].includes(oct3)) { issues.push(`IP ${internalIp} is not allowed in ${environment} environment`); } } // 예약된 IP 검증 const reservedLastOctets = [0, 1, 255]; // 네트워크, 게이트웨이, 브로드캐스트 if (reservedLastOctets.includes(oct4)) { issues.push(`Last octet ${oct4} is reserved (0, 1, 255 not allowed)`); } // 중복 검증 if (this.usedResources.ips.has(internalIp)) { issues.push(`IP address '${internalIp}' is already in use`); } // Ping 테스트로 실제 사용 여부 확인 let isReachable = false; try { execSync(`ping -c 1 -W 1000 ${internalIp}`, { stdio: 'ignore' }); isReachable = true; issues.push(`IP address '${internalIp}' is already reachable (possibly in use)`); } catch (error) { // Ping 실패 - IP 사용 가능 isReachable = false; } return { valid: issues.length === 0, internalIp: internalIp, environment: environment, isReachable: isReachable, issues: issues, allowedRanges: environmentRanges[environment]?.map(oct3 => `192.168.${oct3}.2-254`) || [], rules: { format: 'valid IPv4 format', range: 'private address ranges only', environment: `${environment} environment restrictions`, reserved: 'avoid .0, .1, .255' } }; } /** * 네트워크 연결성 검증 */ async validateNetworkConnectivity(internalIp) { const issues = []; try { // 게이트웨이 연결성 확인 const gateway = internalIp.replace(/\.\d+$/, '.1'); try { execSync(`ping -c 1 -W 1000 ${gateway}`, { stdio: 'ignore' }); } catch (error) { issues.push(`Cannot reach gateway ${gateway}`); } // 네트워크 인터페이스 확인 const interfaces = execSync('ip route show', { encoding: 'utf8' }); const hasMatchingRoute = interfaces.includes(internalIp.replace(/\.\d+$/, '.0/24')); if (!hasMatchingRoute) { issues.push(`No routing entry found for network ${internalIp.replace(/\.\d+$/, '.0/24')}`); } } catch (error) { issues.push(`Network connectivity check failed: ${error.message}`); } return { valid: issues.length === 0, internalIp: internalIp, issues: issues }; } /** * 리소스 가용성 통합 검증 */ async validateResourceAvailability(dockerId, domain, internalIp) { const issues = []; // 포트 가용성 확인 (80, 443) const commonPorts = [80, 443]; for (const port of commonPorts) { try { const netstat = execSync(`netstat -tln | grep :${port}`, { encoding: 'utf8' }); if (netstat.includes(`${internalIp}:${port}`)) { issues.push(`Port ${port} is already in use on ${internalIp}`); } } catch (error) { // Port not in use - good } } // 시스템 리소스 확인 try { const memInfo = execSync('free -m', { encoding: 'utf8' }); const memMatch = memInfo.match(/Mem:\s+\d+\s+\d+\s+(\d+)/); const availableMem = memMatch ? parseInt(memMatch[1]) : 0; if (availableMem < 512) { // 512MB 미만 issues.push(`Low memory available: ${availableMem}MB (recommend 512MB+)`); } } catch (error) { console.warn('Could not check memory availability:', error.message); } return { valid: issues.length === 0, resources: { dockerId: dockerId, domain: domain, internalIp: internalIp }, issues: issues }; } /** * 권장사항 생성 */ generateRecommendations(validationResults) { const recommendations = []; // Docker ID 권장사항 if (!validationResults.dockerId.valid) { recommendations.push({ type: 'dockerId', suggestion: 'Use lowercase letters, numbers, and hyphens only. Avoid docker- prefix.', example: 'webapp-frontend-v2' }); } // Domain 권장사항 if (!validationResults.domain.valid) { recommendations.push({ type: 'domain', suggestion: 'Use a valid domain format with your organization subdomain.', example: 'myapp.internal.company.com' }); } // IP 권장사항 if (!validationResults.internalIp.valid) { const availableRange = '192.168.101.10-192.168.101.254'; recommendations.push({ type: 'internalIp', suggestion: `Use an available IP in the range: ${availableRange}`, example: this.suggestAvailableIp() }); } return recommendations; } /** * 사용 가능한 IP 제안 */ suggestAvailableIp() { for (let lastOctet = 10; lastOctet <= 254; lastOctet++) { const candidateIp = `192.168.101.${lastOctet}`; if (!this.usedResources.ips.has(candidateIp)) { return candidateIp; } } return '192.168.101.10'; // 기본값 } /** * 리소스 캐시 업데이트 */ async updateResourceCache() { const now = Date.now(); if (this.lastCacheUpdate && (now - this.lastCacheUpdate) < this.CACHE_TTL) { return; // 캐시가 유효함 } try { // Docker 컨테이너 목록 업데이트 const containers = execSync('docker ps --format "{{.Names}}"', { encoding: 'utf8' }); this.usedResources.dockerIds = new Set( containers.split('\n').filter(name => name.trim()) ); // 사용 중인 IP 목록 업데이트 (간단한 예시) // 실제로는 더 정교한 네트워크 스캔이 필요 this.usedResources.ips.clear(); this.lastCacheUpdate = now; console.log('🔄 Resource cache updated'); } catch (error) { console.warn('Failed to update resource cache:', error.message); } } /** * 자동 IP 할당 */ async autoAllocateIp(environment = 'dev') { await this.updateResourceCache(); const baseOctet = environment === 'prod' ? 102 : 101; for (let lastOctet = 10; lastOctet <= 254; lastOctet++) { const candidateIp = `192.168.${baseOctet}.${lastOctet}`; if (!this.usedResources.ips.has(candidateIp)) { // Ping 테스트로 실제 사용 여부 확인 try { execSync(`ping -c 1 -W 1000 ${candidateIp}`, { stdio: 'ignore' }); // IP가 응답하면 사용 중 continue; } catch (error) { // Ping 실패 - 사용 가능 return candidateIp; } } } throw new Error(`No available IP addresses in 192.168.${baseOctet}.x range`); } } export default DeploymentValidator;