diff --git a/packages/terraform/parser/BaseResourceParser.ts b/packages/terraform/parser/BaseResourceParser.ts new file mode 100644 index 00000000..95513264 --- /dev/null +++ b/packages/terraform/parser/BaseResourceParser.ts @@ -0,0 +1,25 @@ +import { ResourceParsingStrategy } from '../util/interface/ResourceParsingStrategy'; +import { NCloudModel } from '../interface/NCloudModel'; + +export abstract class BaseResourceParser implements ResourceParsingStrategy { + protected abstract resourceType: string[]; + + protected abstract createModel(properties: any): NCloudModel; + + canParse(type: string): boolean { + return this.resourceType.includes(type.toLowerCase()); + } + + parse(properties: any): NCloudModel { + this.validateProperties(properties); + return this.createModel(properties); + } + + protected validateProperties(properties: any): void {} + + protected getNameOrDefault(properties: any, defaultPrefix: string): string { + return ( + properties.name?.toLowerCase() || `${defaultPrefix}-${Date.now()}` + ); + } +} diff --git a/packages/terraform/parser/MySQLParesr.ts b/packages/terraform/parser/MySQLParesr.ts new file mode 100644 index 00000000..0995cf69 --- /dev/null +++ b/packages/terraform/parser/MySQLParesr.ts @@ -0,0 +1,83 @@ +import { BaseResourceParser } from './BaseResourceParser'; +import { ValidationError } from '../util/ValidationError'; +import { NCloudModel } from '../interface/NCloudModel'; +import { NCloudMySQL } from '../model/NCloudMySQL'; + +export class MySQLParesr extends BaseResourceParser { + protected resourceType = ['db-mysql', 'mysql']; + private validationRules: MySQLValidationRules[] = [ + { + fieldName: 'serviceName', + minLength: 3, + maxLength: 30, + required: true, + }, + { + fieldName: 'serverNamePrefix', + minLength: 3, + maxLength: 20, + required: true, + }, + { fieldName: 'userName', minLength: 4, maxLength: 16, required: true }, + { + fieldName: 'userPassword', + minLength: 8, + maxLength: 20, + required: true, + }, + { fieldName: 'hostIp', required: true }, + { + fieldName: 'databaseName', + minLength: 1, + maxLength: 30, + required: true, + }, + ]; + + protected validateProperties(properties: any) { + for (const rule of this.validationRules) { + this.validateField(properties, rule); + } + } + + protected validateField(properties: any, rule: MySQLValidationRules): void { + const value = properties[rule.fieldName]; + + if (rule.required && !value) { + throw new ValidationError( + 'MySQL', + rule.fieldName, + '필수 속성이 없습니다.', + ); + } + + if (value && rule.minLength && value.length < rule.minLength) { + throw new ValidationError( + 'MySQL', + rule.fieldName, + '최소 길이보다 짧습니다.', + ); + } + + if (value && rule.maxLength && value.length > rule.maxLength) { + throw new ValidationError( + 'MySQL', + rule.fieldName, + '최대 길이를 초과했습니다.', + ); + } + } + + protected createModel(properties: any): NCloudModel { + return new NCloudMySQL({ + serviceName: properties.serviceName || 'mysql', + serverNamePrefix: properties.serverNamePrefix, + userName: properties.userName, + userPassword: properties.userPassword, + hostIp: properties.hostIp, + databaseName: properties.databaseName, + subnet: properties.subnet, + vpc: properties.vpc, + }); + } +} diff --git a/packages/terraform/parser/ParserMetrics.ts b/packages/terraform/parser/ParserMetrics.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/terraform/parser/ResourceParserFactory.ts b/packages/terraform/parser/ResourceParserFactory.ts new file mode 100644 index 00000000..8f999748 --- /dev/null +++ b/packages/terraform/parser/ResourceParserFactory.ts @@ -0,0 +1,32 @@ +import { ResourceParsingStrategy } from '../util/interface/ResourceParsingStrategy'; +import { VPCParser } from './VPCParser'; +import { NCloudModel } from '../interface/NCloudModel'; +import { MySQLParesr } from './MySQLParesr'; +import { ServerParser } from './ServerParser'; +import { SubnetParser } from './SubnetParser'; + +export class ResourceParserFactory { + private static strategy: ResourceParsingStrategy[] = [ + new VPCParser(), + new MySQLParesr(), + new ServerParser(), + new SubnetParser() + ]; + + static getParser(type: string): ResourceParsingStrategy{ + const parser = this.strategy.find(s => s.canParse(type)); + if(!parser) { + throw new Error('올바르지 않은 리소스 타입입니다'); + } + return parser; + } + + static parseResource(type: string, properties: any): NCloudModel{ + const parser = this.getParser(type); + return parser.parse(properties); + } + + static registerParser(parser: ResourceParsingStrategy): void { + this.strategy.push(parser); + } +} diff --git a/packages/terraform/parser/ServerParser.ts b/packages/terraform/parser/ServerParser.ts new file mode 100644 index 00000000..71313850 --- /dev/null +++ b/packages/terraform/parser/ServerParser.ts @@ -0,0 +1,43 @@ +import { BaseResourceParser } from './BaseResourceParser'; +import { ValidationError } from '../util/ValidationError'; +import { NCloudServer } from '../model/NCloudServer'; + +export class ServerParser extends BaseResourceParser { + protected resourceType = ['server']; + + protected validateProperties(properties: any): void { + if (!properties.server_image_number) { + throw new ValidationError( + 'Server', + 'serverImageNumber', + 'server image number 가 필수입니다.', + ); + } + if (!properties.server_spec_code) { + throw new ValidationError( + 'Server', + 'serverSpecCode', + 'server spec code 가 필수입니다.', + ); + } + if (!properties.subnet) { + throw new ValidationError( + 'Server', + 'subnet', + 'subnet 이 필수입니다.', + ); + } + } + + protected createModel(properties: any): NCloudServer { + return new NCloudServer({ + name: this.getNameOrDefault(properties, 'server'), + serverImageNumber: properties.server_image_number, + serverSpecCode: properties.server_spec_code, + subnetName: properties.subnet, + loginKeyName: properties.loginKeyName, + nicName: properties.nicName, + acgName: properties.acgName, + }); + } +} diff --git a/packages/terraform/parser/SubnetParser.ts b/packages/terraform/parser/SubnetParser.ts new file mode 100644 index 00000000..ce09ad4f --- /dev/null +++ b/packages/terraform/parser/SubnetParser.ts @@ -0,0 +1,56 @@ +import { BaseResourceParser } from './BaseResourceParser'; +import { ValidationError } from '../util/ValidationError'; +import { NCloudSubnet } from '../model/NCloudSubnet'; + +export class SubnetParser extends BaseResourceParser { + protected resourceType = ['subnet']; + + protected validateProperties(properties: any): void { + if (!properties.subnet) { + throw new ValidationError( + 'Subnet', + 'subnet', + 'CIDR block 이 필수 속성입니다.', + ); + } + if (!properties.zone) { + throw new ValidationError( + 'Subnet', + 'zone', + 'zone 이 필수 속성입니다.', + ); + } + if ( + !properties.subnetType || + !['PUBLIC', 'PRIVATE'].includes(properties.subnetType) + ) { + throw new ValidationError( + 'Subnet', + 'subnetType', + 'PUBLIC 또는 PRIVATE 이어야 합니다', + ); + } + if ( + properties.usageType && + !['GEN', 'LOADB', 'BM', 'NATGW'].includes(properties.usageType) + ) { + throw new ValidationError( + 'Subnet', + 'usageType', + 'GEN, LOADB, BM, NATGW 중 하나여야 합니다', + ); + } + } + + protected createModel(properties: any): NCloudSubnet { + return new NCloudSubnet({ + name: this.getNameOrDefault(properties, 'subnet'), + subnet: properties.subnet, + zone: properties.zone, + subnetType: properties.subnetType, + usageType: properties.usageType || 'GEN', + vpcName: properties.vpcName, + networkAclNo: properties.networkAclNo, + }); + } +} diff --git a/packages/terraform/parser/VPCParser.ts b/packages/terraform/parser/VPCParser.ts new file mode 100644 index 00000000..b2953f06 --- /dev/null +++ b/packages/terraform/parser/VPCParser.ts @@ -0,0 +1,24 @@ +import { BaseResourceParser } from './BaseResourceParser'; +import { ValidationError } from '../util/ValidationError'; +import { NCloudVPC } from '../model/NCloudVPC'; + +export class VPCParser extends BaseResourceParser { + protected resourceType = ['vpc']; + + protected validateProperties(properties: any) { + if (!properties.cidrBlock) { + throw new ValidationError( + 'VPC', + 'ipv4CidrBlock', + '필수 속성이 없습니다.', + ); + } + } + + protected createModel(properties: any): NCloudVPC { + return new NCloudVPC({ + name: this.getNameOrDefault(properties, 'vpc'), + ipv4CidrBlock: properties.cidrBlock, + }); + } +} diff --git a/packages/terraform/parser/interface/MySQLValidationRules.ts b/packages/terraform/parser/interface/MySQLValidationRules.ts new file mode 100644 index 00000000..089a9560 --- /dev/null +++ b/packages/terraform/parser/interface/MySQLValidationRules.ts @@ -0,0 +1,6 @@ +interface MySQLValidationRules { + fieldName: string, + minLength?: number, + maxLength?: number, + required: boolean; +} \ No newline at end of file diff --git a/packages/terraform/util/ReferenceMetrics.ts b/packages/terraform/util/ReferenceMetrics.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/terraform/util/ReferenceReplacer.ts b/packages/terraform/util/ReferenceReplacer.ts new file mode 100644 index 00000000..5355c346 --- /dev/null +++ b/packages/terraform/util/ReferenceReplacer.ts @@ -0,0 +1,87 @@ +import { ReferenceMetrics } from './ReferenceMetrics'; + +type ReferenceMap = Map; + +export class ReferenceReplacer { + private referenceCache = new Map(); + + constructor(private resourceNameMap: ReferenceMap) {} + + replaceReferences(properties: { [key: string]: any }): { + [key: string]: any; + } { + const result = { ...properties }; + + for (const [key, value] of Object.entries(result)) { + result[key] = this.transformValue(value); + } + + return result; + } + + private transformValue(value: any): any { + if (typeof value === 'string') { + return this.getCachedReference(value); + } + + if (Array.isArray(value)) { + return value.map((item) => this.transformValue(item)); + } + + if (typeof value === 'object' && value !== null) { + const transformed = { ...value }; + for (const [k, v] of Object.entries(transformed)) { + transformed[k] = this.transformValue(v); + } + return transformed; + } + + return value; + } + + private getCachedReference(value: string): string { + const startTime = performance.now(); + + if (this.referenceCache.has(value)) { + const result = this.referenceCache.get(value)!; + return result; + } + + const resolvedReference = this.resolveReference(value); + this.referenceCache.set(value, resolvedReference); + + return resolvedReference; + } + + private resolveReference(value: string): string { + if (value.startsWith('ncloud_')) { + return value; + } + + const resourceTypeMap = new Map([ + ['vpc', 'ncloud_vpc'], + ['subnet', 'ncloud_subnet'], + ['acg', 'ncloud_access_control_group'], + ['nic', 'ncloud_network_interface'], + ['server', 'ncloud_server'], + ['loginkey', 'ncloud_login_key'], + ['nacl', 'ncloud_network_acl'], + ['publicip', 'ncloud_public_ip'], + ]); + + if (value.endsWith('.default_network_acl_no')) { + const resourceName = value.split('.')[0]; + return `${resourceTypeMap.get('vpc')}.${resourceName}.default_network_acl_no`; + } + + if (value.endsWith('.id')) { + const resourceName = value.split('.')[0]; + const resourceType = resourceTypeMap.get( + resourceName.split('-')[0], + ); + return `${resourceType}.${resourceName}.id`; + } + + return value; + } +} diff --git a/packages/terraform/util/ValidationError.ts b/packages/terraform/util/ValidationError.ts new file mode 100644 index 00000000..165da8bb --- /dev/null +++ b/packages/terraform/util/ValidationError.ts @@ -0,0 +1,11 @@ +export class ValidationError extends Error{ + constructor( + public readonly resource: string, + public readonly field: string, + message: string + ) { + super(`${resource} 에서 ${field}속성이 필요합니다`); + this.name = `ValidationError`; + this.message = message; + } +} \ No newline at end of file diff --git a/packages/terraform/util/interface/ResourceParsingStrategy.ts b/packages/terraform/util/interface/ResourceParsingStrategy.ts new file mode 100644 index 00000000..d813b67b --- /dev/null +++ b/packages/terraform/util/interface/ResourceParsingStrategy.ts @@ -0,0 +1,6 @@ +import { NCloudModel } from '../../interface/NCloudModel'; + +export interface ResourceParsingStrategy{ + parse(properties: any): NCloudModel; + canParse(type: string): boolean; +} \ No newline at end of file diff --git a/packages/terraform/util/reference.ts b/packages/terraform/util/reference.ts index 9468e4d0..8e67c4b7 100644 --- a/packages/terraform/util/reference.ts +++ b/packages/terraform/util/reference.ts @@ -1,3 +1,5 @@ +import { ReferenceReplacer } from './ReferenceReplacer'; + type ReferenceMap = Map; export const resolveReference = (resourceId: string): string => { @@ -30,24 +32,10 @@ export const resolveReference = (resourceId: string): string => { return resourceId; }; -export const replaceReferences = ( +export function replaceReferences( properties: { [key: string]: any }, resourceNameMap: ReferenceMap, -): { [key: string]: any } => { - const result = { ...properties }; - for (const [key, value] of Object.entries(result)) { - if (typeof value === 'string') { - result[key] = resolveReference(value); - } else if (Array.isArray(value)) { - result[key] = value.map((item) => - typeof item === 'string' - ? resolveReference(item) - : replaceReferences({ value: item }, resourceNameMap).value, - ); - } else if (typeof value === 'object' && value !== null) { - result[key] = replaceReferences(value, resourceNameMap); - } - } - - return result; -}; +): { [key: string]: any } { + const replacer = new ReferenceReplacer(resourceNameMap); + return replacer.replaceReferences(properties); +} diff --git a/packages/terraform/util/resourceParser.ts b/packages/terraform/util/resourceParser.ts index 08775324..9f0b05f1 100644 --- a/packages/terraform/util/resourceParser.ts +++ b/packages/terraform/util/resourceParser.ts @@ -1,163 +1,7 @@ +import { ResourceParserFactory } from '../parser/ResourceParserFactory'; import { NCloudModel } from '../interface/NCloudModel'; -import { NCloudVPC } from '../model/NCloudVPC'; -import { NCloudNetworkACL } from '../model/NCloudNetworkACL'; -import { NCloudSubnet } from '../model/NCloudSubnet'; -import { NCloudACG } from '../model/NCloudACG'; -import { NCloudACGRule } from '../model/NCloudACGRule'; -import { NCloudLoginKey } from '../model/NCloudLoginKey'; -import { NCloudNetworkInterface } from '../model/NCloudNetworkInterface'; -import { NCloudServer } from '../model/NCloudServer'; -import { NCloudPublicIP } from '../model/NCloudPublicIP'; -import { NCloudLoadBalancer } from '../model/NCloudLoadBalancer'; -import { NCloudLaunchConfiguration } from '../model/NCloudLaunchConfiguration'; -import { NCloudMySQL } from '../model/NCloudMySQL'; -import { NCloudObjectStorageBucket } from '../model/NCloudObjectStorageBucket'; -import { NCloudRedis } from '../model/NCloudRedis'; -import { NCloudNKsCluster } from '../model/NCloudNKsCluster'; export function parseToNCloudModel(resource: any): NCloudModel { const { type, properties } = resource; - - switch (type.toLowerCase()) { - case 'vpc': - return new NCloudVPC({ - name: properties.name || 'vpc', - ipv4CidrBlock: properties.cidrBlock, - }); - - case 'networkacl': - return new NCloudNetworkACL({ - name: properties.name || 'nacl', - vpcName: properties.vpcName, - }); - - case 'subnet': - return new NCloudSubnet({ - name: properties.name || 'subnet', - subnet: properties.subnet, - zone: properties.zone, - subnetType: properties.subnetType, - usageType: properties.usageType, - vpcName: properties.vpcName, - networkAclNo: properties.networkAclNo, - }); - - case 'acg': - case 'accesscontrolgroup': - return new NCloudACG({ - name: properties.name || 'acg', - description: properties.description, - vpcName: properties.vpcName, - }); - - case 'acgrule': - case 'accesscontrolgrouprule': - return new NCloudACGRule({ - protocol: properties.protocol, - ipBlock: properties.ip_block, - portRange: properties.port_range, - description: properties.description, - acgName: properties.acgName, - name: name, - }); - - case 'loginkey': - return new NCloudLoginKey({ - name: properties.name || 'login-key', - }); - - case 'networkinterface': - return new NCloudNetworkInterface({ - name: properties.name || 'nic', - subnetName: properties.subnetName, - acgName: properties.acgName, - }); - - case 'server': - return new NCloudServer({ - name: properties.name || 'server', - serverImageNumber: properties.server_image_number, - serverSpecCode: properties.server_spec_code, - subnetName: properties.subnet, - }); - - case 'publicip': - return new NCloudPublicIP({ - name: properties.name || 'public-ip', - description: properties.description, - serverName: properties.serverName, - }); - - case 'loadbalancer': - return new NCloudLoadBalancer({ - name: properties.name || 'load-balancer', - networkType: properties.networkType, - type: properties.type, - subnetName: properties.subnet, - vpcName: properties.vpc, - }); - - case 'launchconfiguration': - return new NCloudLaunchConfiguration({ - name: properties.name || 'launch-config', - serverImageProductCode: properties.serverImageProductCode, - serverProductCode: properties.serverProductCode, - }); - - case 'db-mysql': - if ( - !properties.serverNamePrefix || - !properties.userName || - !properties.userPassword || - !properties.hostIp || - !properties.databaseName - ) { - throw new Error( - 'userPassword, histIp, databaseName이 필요합니다', - ); - } - return new NCloudMySQL({ - serviceName: properties.serviceName || 'mysql', - serverNamePrefix: properties.serverNamePrefix, - userName: properties.userName, - userPassword: properties.userPassword, - hostIp: properties.hostIp, - databaseName: properties.databaseName, - subnet: properties.subnet, - vpc: properties.vpc, - }); - - case 'object-storage': - return new NCloudObjectStorageBucket({ - bucketName: properties.bucketName, - }); - - case 'redis': - return new NCloudRedis({ - serviceName: properties.serviceName || 'redis', - serverNamePrefix: properties.serverNamePrefix, - vpcNo: properties.vpc, - subnetNo: properties.subnet, - configGroupNo: properties.configGroup, - mode: properties.mode, - }); - - case 'nkscluster': - return new NCloudNKsCluster({ - name: name, - hypervisorCode: properties.hypervisorCode, - clusterType: properties.clusterType, - k8sVersion: properties.k8sVersion, - loginKeyName: properties.loginKeyName, - lbPrivateSubnetNo: properties.lbPrivateSubnet, - lbPublicSubnetNo: properties.lbPublicSubnet, - subnetNoList: properties.subnet, - vpcNo: properties.vpc, - subnetNo: properties.subnet, - publicNetwork: properties.publicNetwork, - zone: properties.zone, - }); - default: - throw new Error(`Unsupported resource type: ${type}`); - } -} + return ResourceParserFactory.parseResource(type, properties); +} \ No newline at end of file