Compare commits

..

4 Commits

Author SHA1 Message Date
b30cebb603 v0.0.19 | Internet Gateway / Routing Table 관리 API 추가
All checks were successful
Build And Test / build-and-push (push) Successful in 13s
- ApiVpc에 IGW CRUD 추가 (/v2.0/internetgateways, external_network_id 사용)
- IGW/RoutingTable/Subnet-attach View 및 URL 신규 (모두 동기)
- API_SPEC.md 3.7.1~3.7.8 네트워크 섹션 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:17:39 +09:00
48fc246301 v0.0.18 | VPC 생성 필드명 cidr → cidrv4 (NHN API 명세 및 프론트엔드 일치)
All checks were successful
Build And Test / build-and-push (push) Successful in 16s
2026-04-24 16:27:24 +09:00
8aede4f2f5 ci: Install Docker 스텝 제거
gitea/runner-images:ubuntu-latest 러너 이미지에 Docker가 이미 포함.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:25:35 +09:00
76cd46ec86 ci: version 파일 변경 시에만 트리거하도록 paths 필터 추가
이미지 태그 불변성 보장 및 자동 커밋 루프 방지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:06:37 +09:00
7 changed files with 487 additions and 9 deletions

View File

@ -5,8 +5,12 @@ run-name: ${{ gitea.actor }} is runs ci pipeline
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
paths:
- 'version'
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
paths:
- 'version'
jobs: jobs:
build-and-push: build-and-push:
@ -19,12 +23,6 @@ jobs:
id: img-ver id: img-ver
run: echo "content=$(cat ./version | tr -d '\n')" >> $GITHUB_OUTPUT run: echo "content=$(cat ./version | tr -d '\n')" >> $GITHUB_OUTPUT
- name: Install Docker
run: |
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
if: runner.os == 'Linux'
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1

View File

@ -387,6 +387,107 @@ DELETE /api/nhn/subnet/{subnet_id}/
--- ---
### 3.7.1 서브넷에 라우팅 테이블 연결
```
PUT /api/nhn/subnet/{subnet_id}/attach-routing-table/
```
**Request Body**
```json
{
"routingtable_id": "rt-id-123"
}
```
---
### 3.7.2 인터넷 게이트웨이 목록 조회
```
GET /api/nhn/internet-gateway/
```
**Response (200 OK)** NHN Cloud Internet Gateway Public API `/v2.0/internetgateways` 통과 응답.
---
### 3.7.3 인터넷 게이트웨이 생성
```
POST /api/nhn/internet-gateway/create/
```
**Request Body**
```json
{
"name": "igw-1",
"external_network_id": "ext-net-id (선택, 생략 시 자동 조회)"
}
```
---
### 3.7.4 인터넷 게이트웨이 상세/삭제
```
GET /api/nhn/internet-gateway/{internetgateway_id}/
DELETE /api/nhn/internet-gateway/{internetgateway_id}/
```
---
### 3.7.5 라우팅 테이블 목록 조회
```
GET /api/nhn/routing-table/
```
---
### 3.7.6 라우팅 테이블 생성
```
POST /api/nhn/routing-table/create/
```
**Request Body**
```json
{
"name": "rt-1",
"vpc_id": "vpc-id-123",
"distributed": true
}
```
---
### 3.7.7 라우팅 테이블 상세/삭제
```
GET /api/nhn/routing-table/{routingtable_id}/
DELETE /api/nhn/routing-table/{routingtable_id}/
```
---
### 3.7.8 라우팅 테이블 기본 설정 / 게이트웨이 연결·해제
```
PUT /api/nhn/routing-table/{routingtable_id}/set-default/
PUT /api/nhn/routing-table/{routingtable_id}/attach-gateway/
PUT /api/nhn/routing-table/{routingtable_id}/detach-gateway/
```
**attach-gateway Request Body**
```json
{
"gateway_id": "igw-id-123"
}
```
---
### 3.8 Floating IP 목록 조회 ### 3.8 Floating IP 목록 조회
``` ```

View File

@ -196,6 +196,46 @@ class ApiVpc(BaseAPI):
payload = {"routingtable_id": routingtable_id} payload = {"routingtable_id": routingtable_id}
return self._put(url, payload) return self._put(url, payload)
# ==================== Internet Gateway ====================
def get_internet_gateway_list(self) -> dict:
"""인터넷 게이트웨이 목록 조회"""
url = f"{self.vpc_url}/v2.0/internetgateways"
logger.info(f"[NHN API] IGW 목록 조회 요청 - URL={url}")
result = self._get(url)
count = len(result.get("internetgateways", []))
logger.info(f"[NHN API] IGW 목록 조회 완료 - count={count}")
return result
def get_internet_gateway_info(self, internetgateway_id: str) -> dict:
"""인터넷 게이트웨이 상세 조회"""
url = f"{self.vpc_url}/v2.0/internetgateways/{internetgateway_id}"
return self._get(url)
def create_internet_gateway(self, name: str, external_network_id: str) -> dict:
"""
인터넷 게이트웨이 생성
Args:
name: IGW 이름
external_network_id: 외부 네트워크 ID (get_external_network_id 로 조회)
"""
url = f"{self.vpc_url}/v2.0/internetgateways"
payload = {
"internetgateway": {
"name": name,
"external_network_id": external_network_id,
}
}
logger.info(f"IGW 생성 요청: {name} (external_network={external_network_id})")
return self._post(url, payload)
def delete_internet_gateway(self, internetgateway_id: str) -> dict:
"""인터넷 게이트웨이 삭제"""
url = f"{self.vpc_url}/v2.0/internetgateways/{internetgateway_id}"
logger.info(f"IGW 삭제 요청: {internetgateway_id}")
return self._delete(url)
# ==================== Floating IP ==================== # ==================== Floating IP ====================
def get_external_network_id(self) -> dict: def get_external_network_id(self) -> dict:

View File

@ -99,7 +99,7 @@ class VpcSerializer(serializers.Serializer):
help_text="VPC 이름", help_text="VPC 이름",
max_length=255, max_length=255,
) )
cidr = serializers.CharField( cidrv4 = serializers.CharField(
help_text="CIDR 블록 (예: 10.0.0.0/16)", help_text="CIDR 블록 (예: 10.0.0.0/16)",
) )
@ -119,6 +119,58 @@ class SubnetSerializer(serializers.Serializer):
) )
class SubnetAttachRoutingTableSerializer(serializers.Serializer):
"""서브넷에 라우팅 테이블 연결 요청"""
routingtable_id = serializers.CharField(
help_text="라우팅 테이블 ID",
)
# ==================== Routing Table ====================
class RoutingTableSerializer(serializers.Serializer):
"""라우팅 테이블 생성 요청"""
name = serializers.CharField(
help_text="라우팅 테이블 이름",
max_length=255,
)
vpc_id = serializers.CharField(
help_text="VPC ID",
)
distributed = serializers.BooleanField(
help_text="분산 라우팅 여부 (기본 True)",
default=True,
)
class RoutingTableAttachGatewaySerializer(serializers.Serializer):
"""라우팅 테이블에 인터넷 게이트웨이 연결 요청"""
gateway_id = serializers.CharField(
help_text="인터넷 게이트웨이 ID",
)
# ==================== Internet Gateway ====================
class InternetGatewaySerializer(serializers.Serializer):
"""인터넷 게이트웨이 생성 요청"""
name = serializers.CharField(
help_text="인터넷 게이트웨이 이름",
max_length=255,
)
external_network_id = serializers.CharField(
help_text="외부 네트워크 ID (생략 시 자동 조회)",
required=False,
allow_blank=True,
)
# ==================== Security Group ==================== # ==================== Security Group ====================

View File

@ -28,6 +28,20 @@ urlpatterns = [
path("subnet/", views.SubnetListView.as_view(), name="subnet-list"), path("subnet/", views.SubnetListView.as_view(), name="subnet-list"),
path("subnet/create/", views.SubnetCreateView.as_view(), name="subnet-create"), path("subnet/create/", views.SubnetCreateView.as_view(), name="subnet-create"),
path("subnet/<str:subnet_id>/", views.SubnetDetailView.as_view(), name="subnet-detail"), path("subnet/<str:subnet_id>/", views.SubnetDetailView.as_view(), name="subnet-detail"),
path("subnet/<str:subnet_id>/attach-routing-table/", views.SubnetAttachRoutingTableView.as_view(), name="subnet-attach-routing-table"),
# ==================== Internet Gateway ====================
path("internet-gateway/", views.InternetGatewayListView.as_view(), name="internet-gateway-list"),
path("internet-gateway/create/", views.InternetGatewayCreateView.as_view(), name="internet-gateway-create"),
path("internet-gateway/<str:internetgateway_id>/", views.InternetGatewayDetailView.as_view(), name="internet-gateway-detail"),
# ==================== Routing Table ====================
path("routing-table/", views.RoutingTableListView.as_view(), name="routing-table-list"),
path("routing-table/create/", views.RoutingTableCreateView.as_view(), name="routing-table-create"),
path("routing-table/<str:routingtable_id>/", views.RoutingTableDetailView.as_view(), name="routing-table-detail"),
path("routing-table/<str:routingtable_id>/set-default/", views.RoutingTableSetDefaultView.as_view(), name="routing-table-set-default"),
path("routing-table/<str:routingtable_id>/attach-gateway/", views.RoutingTableAttachGatewayView.as_view(), name="routing-table-attach-gateway"),
path("routing-table/<str:routingtable_id>/detach-gateway/", views.RoutingTableDetachGatewayView.as_view(), name="routing-table-detach-gateway"),
# ==================== Floating IP ==================== # ==================== Floating IP ====================
path("floatingip/", views.FloatingIpListView.as_view(), name="floatingip-list"), path("floatingip/", views.FloatingIpListView.as_view(), name="floatingip-list"),

View File

@ -19,6 +19,10 @@ from .serializers import (
ComputeInstanceSerializer, ComputeInstanceSerializer,
VpcSerializer, VpcSerializer,
SubnetSerializer, SubnetSerializer,
SubnetAttachRoutingTableSerializer,
RoutingTableSerializer,
RoutingTableAttachGatewaySerializer,
InternetGatewaySerializer,
SecurityGroupSerializer, SecurityGroupSerializer,
SecurityGroupUpdateSerializer, SecurityGroupUpdateSerializer,
SecurityGroupRuleSerializer, SecurityGroupRuleSerializer,
@ -434,7 +438,7 @@ class VpcCreateView(NHNBaseView):
region=headers["region"], region=headers["region"],
token=headers["token"], token=headers["token"],
name=serializer.validated_data["name"], name=serializer.validated_data["name"],
cidr=serializer.validated_data["cidr"], cidr=serializer.validated_data["cidrv4"],
) )
return Response( return Response(
{"task_id": str(task.id), "status": task.status, "message": "VPC 생성 작업이 시작되었습니다."}, {"task_id": str(task.id), "status": task.status, "message": "VPC 생성 작업이 시작되었습니다."},
@ -654,6 +658,275 @@ class FloatingIpDetachView(NHNBaseView):
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
# ==================== Internet Gateway API ====================
class InternetGatewayListView(NHNBaseView):
"""인터넷 게이트웨이 목록 조회 API"""
@swagger_auto_schema(
operation_summary="인터넷 게이트웨이 목록 조회",
manual_parameters=[region_header, token_header],
responses={200: "IGW 목록"},
)
def get(self, request):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
return Response(api.get_internet_gateway_list())
except NHNCloudAPIError as e:
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class InternetGatewayCreateView(NHNBaseView):
"""인터넷 게이트웨이 생성 API"""
@swagger_auto_schema(
operation_summary="인터넷 게이트웨이 생성",
manual_parameters=[region_header, token_header],
request_body=InternetGatewaySerializer,
responses={201: "생성된 IGW 정보"},
)
def post(self, request):
headers = get_nhn_headers(request)
serializer = InternetGatewaySerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
api = ApiVpc(headers["region"], headers["token"])
external_network_id = serializer.validated_data.get("external_network_id", "")
if not external_network_id:
networks = api.get_external_network_id().get("networks", [])
if not networks:
return Response(
{"error": "외부 네트워크를 찾을 수 없습니다."},
status=status.HTTP_400_BAD_REQUEST,
)
external_network_id = networks[0].get("id")
result = api.create_internet_gateway(
name=serializer.validated_data["name"],
external_network_id=external_network_id,
)
logger.info(f"[IGW] 생성 성공 - name={serializer.validated_data['name']}")
return Response(result, status=status.HTTP_201_CREATED)
except NHNCloudAPIError as e:
logger.error(f"[IGW] 생성 실패 - error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class InternetGatewayDetailView(NHNBaseView):
"""인터넷 게이트웨이 상세/삭제 API"""
@swagger_auto_schema(
operation_summary="인터넷 게이트웨이 상세 조회",
manual_parameters=[region_header, token_header],
responses={200: "IGW 상세 정보"},
)
def get(self, request, internetgateway_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
return Response(api.get_internet_gateway_info(internetgateway_id))
except NHNCloudAPIError as e:
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_summary="인터넷 게이트웨이 삭제",
manual_parameters=[region_header, token_header],
responses={204: "삭제 완료"},
)
def delete(self, request, internetgateway_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
api.delete_internet_gateway(internetgateway_id)
logger.info(f"[IGW] 삭제 성공 - id={internetgateway_id}")
return Response(status=status.HTTP_204_NO_CONTENT)
except NHNCloudAPIError as e:
logger.error(f"[IGW] 삭제 실패 - id={internetgateway_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
# ==================== Routing Table API ====================
class RoutingTableListView(NHNBaseView):
"""라우팅 테이블 목록 조회 API"""
@swagger_auto_schema(
operation_summary="라우팅 테이블 목록 조회",
manual_parameters=[region_header, token_header],
responses={200: "라우팅 테이블 목록"},
)
def get(self, request):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
return Response(api.get_routing_table_list())
except NHNCloudAPIError as e:
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class RoutingTableCreateView(NHNBaseView):
"""라우팅 테이블 생성 API"""
@swagger_auto_schema(
operation_summary="라우팅 테이블 생성",
manual_parameters=[region_header, token_header],
request_body=RoutingTableSerializer,
responses={201: "생성된 라우팅 테이블 정보"},
)
def post(self, request):
headers = get_nhn_headers(request)
serializer = RoutingTableSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
api = ApiVpc(headers["region"], headers["token"])
result = api.create_routing_table(
name=serializer.validated_data["name"],
vpc_id=serializer.validated_data["vpc_id"],
distributed=serializer.validated_data.get("distributed", True),
)
logger.info(f"[RT] 생성 성공 - name={serializer.validated_data['name']}")
return Response(result, status=status.HTTP_201_CREATED)
except NHNCloudAPIError as e:
logger.error(f"[RT] 생성 실패 - error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class RoutingTableDetailView(NHNBaseView):
"""라우팅 테이블 상세/삭제 API"""
@swagger_auto_schema(
operation_summary="라우팅 테이블 상세 조회",
manual_parameters=[region_header, token_header],
responses={200: "라우팅 테이블 상세"},
)
def get(self, request, routingtable_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
return Response(api.get_routing_table_info(routingtable_id))
except NHNCloudAPIError as e:
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_summary="라우팅 테이블 삭제",
manual_parameters=[region_header, token_header],
responses={204: "삭제 완료"},
)
def delete(self, request, routingtable_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
api.delete_routing_table(routingtable_id)
logger.info(f"[RT] 삭제 성공 - id={routingtable_id}")
return Response(status=status.HTTP_204_NO_CONTENT)
except NHNCloudAPIError as e:
logger.error(f"[RT] 삭제 실패 - id={routingtable_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class RoutingTableSetDefaultView(NHNBaseView):
"""라우팅 테이블을 기본으로 설정"""
@swagger_auto_schema(
operation_summary="라우팅 테이블 기본 설정",
manual_parameters=[region_header, token_header],
responses={200: "설정 결과"},
)
def put(self, request, routingtable_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
result = api.set_default_routing_table(routingtable_id)
logger.info(f"[RT] 기본 설정 성공 - id={routingtable_id}")
return Response(result)
except NHNCloudAPIError as e:
logger.error(f"[RT] 기본 설정 실패 - id={routingtable_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class RoutingTableAttachGatewayView(NHNBaseView):
"""라우팅 테이블에 인터넷 게이트웨이 연결"""
@swagger_auto_schema(
operation_summary="라우팅 테이블에 인터넷 게이트웨이 연결",
manual_parameters=[region_header, token_header],
request_body=RoutingTableAttachGatewaySerializer,
responses={200: "연결 결과"},
)
def put(self, request, routingtable_id):
headers = get_nhn_headers(request)
serializer = RoutingTableAttachGatewaySerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
api = ApiVpc(headers["region"], headers["token"])
result = api.attach_gateway_to_routing_table(
routingtable_id=routingtable_id,
gateway_id=serializer.validated_data["gateway_id"],
)
logger.info(f"[RT] 게이트웨이 연결 성공 - rt={routingtable_id}")
return Response(result)
except NHNCloudAPIError as e:
logger.error(f"[RT] 게이트웨이 연결 실패 - rt={routingtable_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class RoutingTableDetachGatewayView(NHNBaseView):
"""라우팅 테이블에서 인터넷 게이트웨이 분리"""
@swagger_auto_schema(
operation_summary="라우팅 테이블에서 인터넷 게이트웨이 분리",
manual_parameters=[region_header, token_header],
responses={200: "분리 결과"},
)
def put(self, request, routingtable_id):
headers = get_nhn_headers(request)
try:
api = ApiVpc(headers["region"], headers["token"])
result = api.detach_gateway_from_routing_table(routingtable_id)
logger.info(f"[RT] 게이트웨이 분리 성공 - rt={routingtable_id}")
return Response(result)
except NHNCloudAPIError as e:
logger.error(f"[RT] 게이트웨이 분리 실패 - rt={routingtable_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
class SubnetAttachRoutingTableView(NHNBaseView):
"""서브넷에 라우팅 테이블 연결"""
@swagger_auto_schema(
operation_summary="서브넷에 라우팅 테이블 연결",
manual_parameters=[region_header, token_header],
request_body=SubnetAttachRoutingTableSerializer,
responses={200: "연결 결과"},
)
def put(self, request, subnet_id):
headers = get_nhn_headers(request)
serializer = SubnetAttachRoutingTableSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
api = ApiVpc(headers["region"], headers["token"])
result = api.attach_routing_table_to_subnet(
subnet_id=subnet_id,
routingtable_id=serializer.validated_data["routingtable_id"],
)
logger.info(f"[Subnet] 라우팅 테이블 연결 성공 - subnet={subnet_id}")
return Response(result)
except NHNCloudAPIError as e:
logger.error(f"[Subnet] 라우팅 테이블 연결 실패 - subnet={subnet_id}, error={e.message}")
return Response({"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
# ==================== Security Group API ==================== # ==================== Security Group API ====================

View File

@ -1 +1 @@
v0.0.17 v0.0.19