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
JavaScript
/**
* 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;