Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NCloud Terraform 변환 코드 구현 #133

Merged
merged 24 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb1286c
✨ Feat: terraform을 이용하여 NCloud server 생성하는 코드 구현
Gdm0714 Nov 18, 2024
0cde358
✨ Feat: 각 리소스 인터페이스 정의
Gdm0714 Nov 19, 2024
815b67a
✨ Feat: nCloud 모델 구현
Gdm0714 Nov 19, 2024
a3ef4a6
✨ Feat: nCloud Resource 모델 구현
Gdm0714 Nov 19, 2024
77a62bb
✨ Feat: networkACL, PublicIP, LoginKey 모델 구현
Gdm0714 Nov 19, 2024
5e4a16f
✨ Feat: 모델 priority 속성 추가하여 실행 순서 제어
Gdm0714 Nov 19, 2024
3647eb7
✨ Feat: netwrokACL interface추가 및 NCloud 모델에 priority 추가
Gdm0714 Nov 19, 2024
1f073cf
✨ Feat: resource Priority enum 타입 추가
Gdm0714 Nov 19, 2024
25afd78
✨ Feat: 테라폼 코드로 바꿔주는 terraform Convertor 구현
Gdm0714 Nov 19, 2024
9fc7a0c
✨ Feat: terraform convertor 테스트 가능한 main 구현
Gdm0714 Nov 19, 2024
b279228
🤖 Refactor: 테라폼 서버 생성 관련 파일 삭제 및 variables.tf 재생성
Gdm0714 Nov 19, 2024
072b46c
💄 Style: terraform convertor 코드 포맷 변경
Gdm0714 Nov 20, 2024
b9e0a15
🤖 Refactor: 불필요한 import 삭제
Gdm0714 Nov 20, 2024
74cf37b
✨ Feat: jSON 데이터를 받아 파싱하기 위한 interface 정의
Gdm0714 Nov 20, 2024
0f6748a
🤖 Refactor: 필요없는 import 제거
Gdm0714 Nov 20, 2024
17435ac
✨ Feat: resource json Data 받아서 모델로 파싱해주는 함수 구현
Gdm0714 Nov 20, 2024
a51c087
✨ Feat: resource json Data를 받아서 모델을 이용해 파싱
Gdm0714 Nov 20, 2024
4cd75bf
✨ Feat: nCloud subnet 참조값 처리 구현
Gdm0714 Nov 21, 2024
bac4156
✨ Feat: 나머지 리소스들에 대한 참조값 처리 구현
Gdm0714 Nov 21, 2024
f9525d2
🐞 Fix: json 파싱에서 properties안에 있던 name 분리
Gdm0714 Nov 21, 2024
3112fe4
🐞 Fix: 배열 리소스 참조 처리 개선
Gdm0714 Nov 21, 2024
df18552
🤖 Refactor: terraformConvertor 각 역할이 명확하게 파일 분리
Gdm0714 Nov 22, 2024
7b38f1c
🤖 Refactor: 실행 테스트 파일 각 기능이 명확하게 파일 분리
Gdm0714 Nov 22, 2024
60f46ea
🤖 Refactor: terrformConvertor 각 기능이 명확하게 파일 분리
Gdm0714 Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions apps/terraform/convertor/TerraformConvertor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { NCloudModel } from '../interface/NCloudModel';
import { NCloudProvider } from '../model/NCloudProvider';
import { CloudCanvasNode } from '../interface/CloudCanvasNode';
import { parseToNCloudModel } from '../util/resourceParser';

export class TerraformConvertor {
private resources: NCloudModel[];
private provider: NCloudProvider;
private resourceNameMap: Map<string, string>;

constructor(provider: NCloudProvider) {
this.provider = provider;
this.resources = [];
this.resourceNameMap = new Map();
}

addResourceFromJson(jsonData: { nodes?: CloudCanvasNode[] }) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 jsonData를 객체로 설정하신 이유가 있나요? 바로 CloudCanvasNode[] 배열을 받는건 어떤가요?

jsonData.nodes?.forEach((node => {
try {
const resource = parseToNCloudModel(node);
this.addResource(resource);
} catch (error) {
console.warn(`Skipping unsupported node type: ${node.type}`);
}
}));
}

addResource(resource: NCloudModel) {
this.resources.push(resource);
this.resourceNameMap.set(resource.serviceType, resource.name);
}



private replaceReferences(properties: { [key: string]: any }): { [key: string]: any } {
const result = { ...properties };

for (const [key, value] of Object.entries(result)) {
if (typeof value === 'string') {
switch(value) {
case 'VPC_ID_PLACEHOLDER':
const vpcName = this.resourceNameMap.get('ncloud_vpc');
result[key] = `ncloud_vpc.${vpcName}.id`;
break;
case 'VPC_ACL_PLACEHOLDER':
const vpcAclName = this.resourceNameMap.get('ncloud_vpc');
result[key] = `ncloud_vpc.${vpcAclName}.default_network_acl_no`;
break;
case 'SUBNET_ID_PLACEHOLDER':
const subnetName = this.resourceNameMap.get('ncloud_subnet');
result[key] = `ncloud_subnet.${subnetName}.id`;
break;
case 'ACG_ID_PLACEHOLDER':
const acgName = this.resourceNameMap.get('ncloud_access_control_group');
result[key] = `ncloud_access_control_group.${acgName}.id`;
break;
case 'LOGIN_KEY_NAME_PLACEHOLDER':
const loginKeyName = this.resourceNameMap.get('ncloud_login_key');
result[key] = `ncloud_login_key.${loginKeyName}.key_name`;
break;
case 'NIC_ID_PLACEHOLDER':
const nicName = this.resourceNameMap.get('ncloud_network_interface');
result[key] = `ncloud_network_interface.${nicName}.id`;
break;
case 'SERVER_ID_PLACEHOLDER':
const serverName = this.resourceNameMap.get('ncloud_server');
result[key] = `ncloud_server.${serverName}.id`;
break;
}
} else if (Array.isArray(value)) {
result[key] = value.map(item => {
if (typeof item === 'string') {
const replacedValue = this.replaceReferences({ temp: item }).temp;
return replacedValue;
}
return this.replaceReferences(item);
});
} else if (typeof value === 'object' && value !== null) {
result[key] = this.replaceReferences(value);
}
}

return result;
}

private formatValue(value: any): string {
if (Array.isArray(value)) {
return `[${value.map(item => this.formatValue(item)).join(', ')}]`;
}

if (typeof value === 'string') {
const ncloudRefPattern = /^ncloud_[a-zA-Z_]+\.[a-zA-Z_-]+\.[a-zA-Z_]+$/;
const varRefPattern = /^var\.[a-zA-Z_]+$/;

if (ncloudRefPattern.test(value) || varRefPattern.test(value)) {
return value;
}

return `"${value}"`;
}
return value;
}

private formatProperties(properties: { [key: string]: any }): string {
const maxKeyLength = Math.max(...Object.keys(properties).map(key => key.length));

return Object.entries(properties)
.map(([key, value]) => {
const padding = ' '.repeat(maxKeyLength - key.length);

if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return ` ${key} {
${this.formatProperties(value)}
}`;
}

return ` ${key}${padding} = ${this.formatValue(value)}`;
})
.join('\n');
}
generate(): string {
const providerProperties = this.provider.getProperties();

const terraformBlock = `
terraform {
required_providers {
ncloud = {
source = "${providerProperties.terraform.required_providers.ncloud.source}"
}
}
required_version = "${providerProperties.terraform.required_version}"
}`;

const providerBlock = `
provider "${this.provider.name}" {
${this.formatProperties(providerProperties.provider)}
}`;

const resourceBlocks = this.resources
.sort((a, b) => a.priority - b.priority)
.map(resource => {
const properties = this.replaceReferences(resource.getProperties());
return `
resource "${resource.serviceType}" "${resource.name}" {
${this.formatProperties(properties)}
}`;
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문자열 템플릿 부분을 utils로 분리하는건 어떨까요

Copy link
Collaborator Author

@Gdm0714 Gdm0714 Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

포크된 레포에는 이미 반영된 상태이고 머지 이후에 다시 pr올릴려고 했는데 그냥 올리겠습니다


return [terraformBlock, providerBlock, ...resourceBlocks].join('\n');
}

async saveToFile(filePath: string): Promise<void> {
const fs = require('fs').promises;
const terraformCode = this.generate();
console.log(terraformCode);
await fs.writeFile(filePath, terraformCode);
}
}
11 changes: 11 additions & 0 deletions apps/terraform/enum/ResourcePriority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum ResourcePriority {
VPC = 1,
NETWORK_ACL = 2,
SUBNET = 3,
ACG = 4,
ACG_RULE = 5,
LOGIN_KEY = 6,
NETWORK_INTERFACE = 7,
SERVER = 8,
PUBLIC_IP = 9
}
6 changes: 6 additions & 0 deletions apps/terraform/interface/ACG.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ACG {
id: string;
name: string;
vpcNo: string;
description: string;
}
6 changes: 6 additions & 0 deletions apps/terraform/interface/ACGRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ACGRule {
protocol: string;
ipBlock: string;
portRange: string;
description: string;
}
6 changes: 6 additions & 0 deletions apps/terraform/interface/CloudCanvasNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface CloudCanvasNode {
id: string;
type: string;
name: string;
properties: { [key: string]: any };
}
8 changes: 8 additions & 0 deletions apps/terraform/interface/NCloudModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ResourcePriority } from '../enum/ResourcePriority';

export interface NCloudModel {
name: string;
serviceType: string;
priority: ResourcePriority;
getProperties(): { [key: string]: any };
}
5 changes: 5 additions & 0 deletions apps/terraform/interface/NetworkACL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface NetworkACL {
id: string;
name: string;
vpcNo: string;
}
6 changes: 6 additions & 0 deletions apps/terraform/interface/NetworkInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface NetworkInterface {
id: string;
name: string;
subnetNo: string;
accessControlGroups: string[];
}
6 changes: 6 additions & 0 deletions apps/terraform/interface/Provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Provider {
accessKey: string;
secretKey: string;
region: string;
site: string;
}
5 changes: 5 additions & 0 deletions apps/terraform/interface/PublicIp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface PublicIp {
id: string;
publicIp: string;
serverInstanceNo: string;
}
9 changes: 9 additions & 0 deletions apps/terraform/interface/Server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Server {
id: string;
name: string;
subnetNo: string;
serverImageProductCode: string;
serverProductCode: string;
loginKeyName: string;
networkInterfaceNo: string;
}
10 changes: 10 additions & 0 deletions apps/terraform/interface/Subnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Subnet {
id: string;
name: string;
vpcNo: string;
subnet: string;
zone: string;
networkAclNo: string;
subnetType: string;
usageType: string;
}
10 changes: 10 additions & 0 deletions apps/terraform/interface/VPC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface VPC {
id: string;
name: string;
region: string;
ipv4CidrBlock: string;
defaultNetworkAclNo: string;
defaultAccessControlGroupNo: string;
defaultPublicRouteTableNo: string;
defaultPrivateRouteTableNo: string;
}
104 changes: 104 additions & 0 deletions apps/terraform/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { CloudCanvasNode } from './interface/CloudCanvasNode';
import { NCloudProvider } from './model/NCloudProvider';
import { TerraformConvertor } from './convertor/TerraformConvertor';

const sampleNodes: CloudCanvasNode[] = [
{
id: "vpc1",
type: "VPC",
name: "my-vpc",
properties: {
cidrBlock: "172.16.0.0/16"
}
},
{
id: "nacl1",
type: "NetworkACL",
name: "my-nacl",
properties: {
}
},
{
id: "subnet1",
type: "Subnet",
name: "my-subnet",
properties: {
subnet: "172.16.10.0/24",
zone: "KR-2",
subnetType: "PUBLIC",
usageType: "GEN"
}
},
{
id: "acg1",
type: "ACG",
name: "my-acg",
properties: {
description: "My ACG"
}
},
{
id: "acgrule1",
type: "ACGRule",
name: "",
properties: {
protocol: "TCP",
ipBlock: "0.0.0.0/0",
portRange: "80",
description: "HTTP"
}
},
{
id: "loginkey1",
type: "LoginKey",
name: "my-key",
properties: {
}
},
{
id: "nic1",
type: "NetworkInterface",
name: "my-nic",
properties: {}
},
{
id: "server1",
type: "Server",
name: "my-server",
properties: {
serverImageProductCode: "SW.VSVR.OS.LNX64.CNTOS.0708.B050",
serverProductCode: "SVR.VSVR.HICPU.C002.M004.NET.HDD.B050.G002"
}
},
{
id: "publicip1",
type: "PublicIP",
name: "my-public-ip",
properties: {}
}
];

async function main() {
try {
const provider = new NCloudProvider({
accessKey: "var.access_key",
secretKey: "var.secret_key",
region: "var.region",
site: "public"
});

const convertor = new TerraformConvertor(provider);

convertor.addResourceFromJson({ nodes: sampleNodes });
await convertor.saveToFile('main.tf');
console.log('\nGenerated Terraform code:');
console.log('----------------------------------------');
console.log(convertor.generate());
console.log('----------------------------------------');

} catch (error) {
console.error('Error generating Terraform configuration:', error);
}
}

main().catch(console.error);
Loading