Skip to content

Commit a23d09d

Browse files
committed
feat(network): add router interface endpoints
1 parent 48f924e commit a23d09d

11 files changed

Lines changed: 414 additions & 17 deletions

internal/api/network/router.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func NewHandler(cfg config.Config) Handler {
3737
storenetwork.NewMemorySecurityGroupRuleRepository(),
3838
storenetwork.NewMemoryRouterRepository(),
3939
storenetwork.NewMemoryFloatingIPRepository(),
40+
storenetwork.NewMemoryRouterInterfaceRepository(),
4041
idgen.Random(),
4142
),
4243
)
@@ -79,6 +80,8 @@ func (h Handler) Router() http.Handler {
7980
router.Post("/routers", h.createRouter)
8081
router.Get("/routers/{router_id}", h.getRouter)
8182
router.Delete("/routers/{router_id}", h.deleteRouter)
83+
router.Put("/routers/{router_id}/add_router_interface", h.addRouterInterface)
84+
router.Put("/routers/{router_id}/remove_router_interface", h.removeRouterInterface)
8285
router.Get("/floatingips", h.listFloatingIPs)
8386
router.Post("/floatingips", h.createFloatingIP)
8487
router.Get("/floatingips/{floating_ip_id}", h.getFloatingIP)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package network
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
8+
"github.com/JSYoo5B/SandStack/internal/api/respond"
9+
appnetwork "github.com/JSYoo5B/SandStack/internal/app/network"
10+
"github.com/go-chi/chi/v5"
11+
)
12+
13+
func (h Handler) addRouterInterface(w http.ResponseWriter, r *http.Request) {
14+
var request routerInterfaceRequest
15+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
16+
respond.Error(w, http.StatusBadRequest, "invalid JSON request body")
17+
return
18+
}
19+
20+
routerInterface, err := h.service.AddRouterInterface(
21+
chi.URLParam(r, "router_id"),
22+
request.routerInterfaceRequest(),
23+
)
24+
if errors.Is(err, appnetwork.ErrRouterNotFound) {
25+
respond.Error(w, http.StatusNotFound, "router not found")
26+
return
27+
}
28+
if err != nil {
29+
respond.Error(w, http.StatusInternalServerError, "router interface add failed")
30+
return
31+
}
32+
33+
respond.JSON(w, http.StatusOK, toRouterInterfaceDocument(routerInterface))
34+
}
35+
36+
func (h Handler) removeRouterInterface(w http.ResponseWriter, r *http.Request) {
37+
var request routerInterfaceRequest
38+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
39+
respond.Error(w, http.StatusBadRequest, "invalid JSON request body")
40+
return
41+
}
42+
43+
routerInterface, err := h.service.RemoveRouterInterface(
44+
chi.URLParam(r, "router_id"),
45+
request.routerInterfaceRequest(),
46+
)
47+
if errors.Is(err, appnetwork.ErrRouterNotFound) {
48+
respond.Error(w, http.StatusNotFound, "router not found")
49+
return
50+
}
51+
if errors.Is(err, appnetwork.ErrRouterInterfaceNotFound) {
52+
respond.Error(w, http.StatusNotFound, "router interface not found")
53+
return
54+
}
55+
if err != nil {
56+
respond.Error(w, http.StatusInternalServerError, "router interface remove failed")
57+
return
58+
}
59+
60+
respond.JSON(w, http.StatusOK, toRouterInterfaceDocument(routerInterface))
61+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package network
2+
3+
import appnetwork "github.com/JSYoo5B/SandStack/internal/app/network"
4+
5+
type routerInterfaceRequest struct {
6+
SubnetID string `json:"subnet_id"`
7+
PortID string `json:"port_id"`
8+
}
9+
10+
func (r routerInterfaceRequest) routerInterfaceRequest() appnetwork.RouterInterfaceRequest {
11+
return appnetwork.RouterInterfaceRequest{
12+
SubnetID: r.SubnetID,
13+
PortID: r.PortID,
14+
}
15+
}
16+
17+
type routerInterfaceDocument struct {
18+
ID string `json:"id"`
19+
SubnetID string `json:"subnet_id"`
20+
PortID string `json:"port_id"`
21+
TenantID string `json:"tenant_id"`
22+
}
23+
24+
func toRouterInterfaceDocument(
25+
routerInterface appnetwork.RouterInterface,
26+
) routerInterfaceDocument {
27+
return routerInterfaceDocument{
28+
ID: routerInterface.ID,
29+
SubnetID: routerInterface.SubnetID,
30+
PortID: routerInterface.PortID,
31+
TenantID: routerInterface.TenantID,
32+
}
33+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package network_test
2+
3+
import (
4+
"net/http/httptest"
5+
"testing"
6+
7+
"github.com/JSYoo5B/SandStack/internal/api/network"
8+
"github.com/JSYoo5B/SandStack/internal/testhelper"
9+
"github.com/gophercloud/gophercloud/v2"
10+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
11+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
12+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
13+
"github.com/stretchr/testify/suite"
14+
)
15+
16+
type RouterInterfaceSuite struct {
17+
suite.Suite
18+
server *httptest.Server
19+
}
20+
21+
func TestRouterInterfaceSuite(t *testing.T) {
22+
suite.Run(t, new(RouterInterfaceSuite))
23+
}
24+
25+
func (s *RouterInterfaceSuite) SetupTest() {
26+
s.server = httptest.NewServer(
27+
network.NewRouter(testhelper.DefaultConfig()),
28+
)
29+
}
30+
31+
func (s *RouterInterfaceSuite) TearDownTest() {
32+
s.server.Close()
33+
}
34+
35+
func (s *RouterInterfaceSuite) TestAddAndRemoveRouterInterface() {
36+
network := s.createNetwork("private")
37+
subnet := s.createSubnet(network.ID, "private-subnet")
38+
router := s.createRouter("edge")
39+
40+
added, err := routers.AddInterface(
41+
s.T().Context(),
42+
testhelper.ServiceClient(s.server.URL),
43+
router.ID,
44+
routers.AddInterfaceOpts{SubnetID: subnet.ID},
45+
).Extract()
46+
s.Require().NoError(err)
47+
s.Require().NotNil(added)
48+
49+
removed, err := routers.RemoveInterface(
50+
s.T().Context(),
51+
testhelper.ServiceClient(s.server.URL),
52+
router.ID,
53+
routers.RemoveInterfaceOpts{SubnetID: subnet.ID},
54+
).Extract()
55+
s.Require().NoError(err)
56+
s.Require().NotNil(removed)
57+
58+
s.Assert().Equal(subnet.ID, added.SubnetID)
59+
s.Assert().NotEmpty(added.PortID)
60+
s.Assert().NotEmpty(added.ID)
61+
s.Assert().Equal(added.ID, removed.ID)
62+
s.Assert().Equal(added.PortID, removed.PortID)
63+
s.Assert().Equal("demo", removed.TenantID)
64+
}
65+
66+
func (s *RouterInterfaceSuite) createNetwork(name string) *networks.Network {
67+
created, err := networks.Create(
68+
s.T().Context(),
69+
testhelper.ServiceClient(s.server.URL),
70+
networks.CreateOpts{
71+
Name: name,
72+
ProjectID: "demo",
73+
},
74+
).Extract()
75+
s.Require().NoError(err)
76+
s.Require().NotNil(created)
77+
78+
return created
79+
}
80+
81+
func (s *RouterInterfaceSuite) createSubnet(
82+
networkID string,
83+
name string,
84+
) *subnets.Subnet {
85+
created, err := subnets.Create(
86+
s.T().Context(),
87+
testhelper.ServiceClient(s.server.URL),
88+
subnets.CreateOpts{
89+
NetworkID: networkID,
90+
Name: name,
91+
CIDR: "192.168.10.0/24",
92+
IPVersion: gophercloud.IPv4,
93+
ProjectID: "demo",
94+
EnableDHCP: boolPtr(true),
95+
},
96+
).Extract()
97+
s.Require().NoError(err)
98+
s.Require().NotNil(created)
99+
100+
return created
101+
}
102+
103+
func (s *RouterInterfaceSuite) createRouter(name string) *routers.Router {
104+
created, err := routers.Create(
105+
s.T().Context(),
106+
testhelper.ServiceClient(s.server.URL),
107+
routers.CreateOpts{
108+
Name: name,
109+
ProjectID: "demo",
110+
},
111+
).Extract()
112+
s.Require().NoError(err)
113+
s.Require().NotNil(created)
114+
115+
return created
116+
}

internal/api/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func NewRouter(cfg config.Config) http.Handler {
6565
storenetwork.NewMemorySecurityGroupRuleRepository(),
6666
storenetwork.NewMemoryRouterRepository(),
6767
storenetwork.NewMemoryFloatingIPRepository(),
68+
storenetwork.NewMemoryRouterInterfaceRepository(),
6869
idgen.Random(),
6970
)
7071
volumeService := appvolume.NewServiceWithRuntime(

internal/app/network/repository.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,10 @@ type FloatingIPRepository interface {
5656
Delete(id string) error
5757
Reset()
5858
}
59+
60+
type RouterInterfaceRepository interface {
61+
Create(routerInterface RouterInterface) RouterInterface
62+
Find(routerID string, request RouterInterfaceRequest) (RouterInterface, error)
63+
Delete(id string) error
64+
Reset()
65+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package network
2+
3+
import "errors"
4+
5+
var ErrRouterInterfaceNotFound = errors.New("router interface not found")
6+
7+
func (s *Service) AddRouterInterface(
8+
routerID string,
9+
request RouterInterfaceRequest,
10+
) (RouterInterface, error) {
11+
router, err := s.routerRepository.Get(routerID)
12+
if err != nil {
13+
return RouterInterface{}, err
14+
}
15+
16+
portID := request.PortID
17+
subnetID := request.SubnetID
18+
if portID == "" {
19+
portID = "port-" + s.idGen.Hex(16)
20+
}
21+
22+
routerInterface := RouterInterface{
23+
ID: "ri-" + s.idGen.Hex(16),
24+
RouterID: routerID,
25+
SubnetID: subnetID,
26+
PortID: portID,
27+
TenantID: router.ProjectID,
28+
}
29+
30+
return s.routerInterfaceRepository.Create(routerInterface), nil
31+
}
32+
33+
func (s *Service) RemoveRouterInterface(
34+
routerID string,
35+
request RouterInterfaceRequest,
36+
) (RouterInterface, error) {
37+
if _, err := s.routerRepository.Get(routerID); err != nil {
38+
return RouterInterface{}, err
39+
}
40+
41+
routerInterface, err := s.routerInterfaceRepository.Find(routerID, request)
42+
if err != nil {
43+
return RouterInterface{}, err
44+
}
45+
46+
if err := s.routerInterfaceRepository.Delete(routerInterface.ID); err != nil {
47+
return RouterInterface{}, err
48+
}
49+
50+
return routerInterface, nil
51+
}

internal/app/network/service.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import (
77
)
88

99
type Service struct {
10-
mu sync.RWMutex
11-
networkRepository NetworkRepository
12-
subnetRepository SubnetRepository
13-
portRepository PortRepository
14-
securityGroupRepository SecurityGroupRepository
15-
securityRuleRepository SecurityGroupRuleRepository
16-
routerRepository RouterRepository
17-
floatingIPRepository FloatingIPRepository
18-
idGen idgen.Generator
10+
mu sync.RWMutex
11+
networkRepository NetworkRepository
12+
subnetRepository SubnetRepository
13+
portRepository PortRepository
14+
securityGroupRepository SecurityGroupRepository
15+
securityRuleRepository SecurityGroupRuleRepository
16+
routerRepository RouterRepository
17+
floatingIPRepository FloatingIPRepository
18+
routerInterfaceRepository RouterInterfaceRepository
19+
idGen idgen.Generator
1920
}
2021

2122
func NewServiceWithRepositories(
@@ -26,17 +27,19 @@ func NewServiceWithRepositories(
2627
securityRuleRepository SecurityGroupRuleRepository,
2728
routerRepository RouterRepository,
2829
floatingIPRepository FloatingIPRepository,
30+
routerInterfaceRepository RouterInterfaceRepository,
2931
idGen idgen.Generator,
3032
) *Service {
3133
return &Service{
32-
networkRepository: networkRepository,
33-
subnetRepository: subnetRepository,
34-
portRepository: portRepository,
35-
securityGroupRepository: securityGroupRepository,
36-
securityRuleRepository: securityRuleRepository,
37-
routerRepository: routerRepository,
38-
floatingIPRepository: floatingIPRepository,
39-
idGen: idGen,
34+
networkRepository: networkRepository,
35+
subnetRepository: subnetRepository,
36+
portRepository: portRepository,
37+
securityGroupRepository: securityGroupRepository,
38+
securityRuleRepository: securityRuleRepository,
39+
routerRepository: routerRepository,
40+
floatingIPRepository: floatingIPRepository,
41+
routerInterfaceRepository: routerInterfaceRepository,
42+
idGen: idGen,
4043
}
4144
}
4245

@@ -51,4 +54,5 @@ func (s *Service) Reset() {
5154
s.securityRuleRepository.Reset()
5255
s.routerRepository.Reset()
5356
s.floatingIPRepository.Reset()
57+
s.routerInterfaceRepository.Reset()
5458
}

0 commit comments

Comments
 (0)