Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 53 additions & 0 deletions internal/api/compute/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package compute

import (
"encoding/json"
"errors"
"net/http"

"github.com/JSYoo5B/SandStack/internal/api/respond"
appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"
"github.com/go-chi/chi/v5"
)

func (h Handler) actionServer(w http.ResponseWriter, r *http.Request) {
var request serverActionRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
respond.Error(w, http.StatusBadRequest, "invalid JSON request body")
return
}

var err error
switch {
case request.Has("os-start"):
err = h.service.StartServer(chi.URLParam(r, "server_id"))
case request.Has("os-stop"):
err = h.service.StopServer(chi.URLParam(r, "server_id"))
case request.Has("reboot"):
err = h.service.RebootServer(chi.URLParam(r, "server_id"))
case request.Has("addSecurityGroup"):
err = h.service.AddServerSecurityGroup(
chi.URLParam(r, "server_id"),
request.SecurityGroupName("addSecurityGroup"),
)
case request.Has("removeSecurityGroup"):
err = h.service.RemoveServerSecurityGroup(
chi.URLParam(r, "server_id"),
request.SecurityGroupName("removeSecurityGroup"),
)
default:
respond.Error(w, http.StatusBadRequest, "unsupported server action")
return
}

if errors.Is(err, appcompute.ErrServerNotFound) {
respond.Error(w, http.StatusNotFound, "server not found")
return
}
if err != nil {
respond.Error(w, http.StatusInternalServerError, "server action failed")
return
}

w.WriteHeader(http.StatusAccepted)
}
18 changes: 18 additions & 0 deletions internal/api/compute/action_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package compute

import "encoding/json"

type serverActionRequest map[string]json.RawMessage

func (r serverActionRequest) Has(action string) bool {
_, ok := r[action]
return ok
}

func (r serverActionRequest) SecurityGroupName(action string) string {
var body struct {
Name string `json:"name"`
}
_ = json.Unmarshal(r[action], &body)
return body.Name
}
126 changes: 126 additions & 0 deletions internal/api/compute/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package compute_test

import (
"net/http/httptest"
"testing"

"github.com/JSYoo5B/SandStack/internal/api/compute"
appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"
"github.com/JSYoo5B/SandStack/internal/platform/clock"
"github.com/JSYoo5B/SandStack/internal/platform/idgen"
storecompute "github.com/JSYoo5B/SandStack/internal/store/compute"
"github.com/JSYoo5B/SandStack/internal/testhelper"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/secgroups"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
"github.com/stretchr/testify/suite"
)

type ActionSuite struct {
suite.Suite
server *httptest.Server
service *appcompute.Service
}

func TestActionSuite(t *testing.T) {
suite.Run(t, new(ActionSuite))
}

func (s *ActionSuite) SetupTest() {
s.service = appcompute.NewServiceWithRuntime(
storecompute.NewMemoryServerRepository(),
storecompute.NewMemoryKeyPairRepository(),
storecompute.NewMemoryServerGroupRepository(),
storecompute.NewMemoryAggregateRepository(),
clock.Wall(),
idgen.Random(),
)
s.server = httptest.NewServer(
compute.NewRouterWithService(
testhelper.DefaultConfig(),
s.service,
),
)
}

func (s *ActionSuite) TearDownTest() {
s.server.Close()
}

func (s *ActionSuite) TestStopAndStartServer() {
created := s.service.CreateServer(appcompute.CreateServer{
Name: "web",
ImageID: "img-1",
FlavorID: "1",
})
client := testhelper.ServiceClient(s.server.URL + "/demo")

err := servers.Stop(
s.T().Context(),
client,
created.ID,
).ExtractErr()
s.Require().NoError(err)

stopped, err := s.service.GetServer(created.ID)
s.Require().NoError(err)
s.Assert().Equal("SHUTOFF", stopped.Status)

err = servers.Start(
s.T().Context(),
client,
created.ID,
).ExtractErr()
s.Require().NoError(err)

started, err := s.service.GetServer(created.ID)
s.Require().NoError(err)
s.Assert().Equal("ACTIVE", started.Status)
}

func (s *ActionSuite) TestRebootServer() {
created := s.service.CreateServer(appcompute.CreateServer{
Name: "web",
ImageID: "img-1",
FlavorID: "1",
})
client := testhelper.ServiceClient(s.server.URL + "/demo")

err := servers.Reboot(
s.T().Context(),
client,
created.ID,
servers.RebootOpts{Type: servers.SoftReboot},
).ExtractErr()
s.Require().NoError(err)

rebooted, err := s.service.GetServer(created.ID)
s.Require().NoError(err)

s.Assert().Equal("ACTIVE", rebooted.Status)
s.Assert().Equal(100, rebooted.Progress)
}

func (s *ActionSuite) TestAddAndRemoveServerSecurityGroup() {
created := s.service.CreateServer(appcompute.CreateServer{
Name: "web",
ImageID: "img-1",
FlavorID: "1",
})
client := testhelper.ServiceClient(s.server.URL + "/demo")

err := secgroups.AddServer(
s.T().Context(),
client,
created.ID,
"default",
).ExtractErr()
s.Require().NoError(err)

err = secgroups.RemoveServer(
s.T().Context(),
client,
created.ID,
"default",
).ExtractErr()
s.Require().NoError(err)
}
46 changes: 46 additions & 0 deletions internal/api/compute/address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package compute

import (
"errors"
"net/http"

"github.com/JSYoo5B/SandStack/internal/api/respond"
appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"
"github.com/go-chi/chi/v5"
)

func (h Handler) listServerAddresses(w http.ResponseWriter, r *http.Request) {
addresses, err := h.service.ServerAddresses(chi.URLParam(r, "server_id"))
if errors.Is(err, appcompute.ErrServerNotFound) {
respond.Error(w, http.StatusNotFound, "server not found")
return
}
if err != nil {
respond.Error(w, http.StatusInternalServerError, "server address lookup failed")
return
}

respond.JSON(w, http.StatusOK, serverAddressListResponse{
Addresses: toServerAddressDocumentsByNetwork(addresses),
})
}

func (h Handler) listServerAddressesByNetwork(w http.ResponseWriter, r *http.Request) {
addresses, err := h.service.ServerAddressesByNetwork(
chi.URLParam(r, "server_id"),
chi.URLParam(r, "network"),
)
if errors.Is(err, appcompute.ErrServerNotFound) {
respond.Error(w, http.StatusNotFound, "server not found")
return
}
if err != nil {
respond.Error(w, http.StatusInternalServerError, "server address lookup failed")
return
}

network := chi.URLParam(r, "network")
respond.JSON(w, http.StatusOK, map[string][]serverAddressDocument{
network: toServerAddressDocuments(addresses),
})
}
37 changes: 37 additions & 0 deletions internal/api/compute/address_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package compute

import appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"

type serverAddressListResponse struct {
Addresses map[string][]serverAddressDocument `json:"addresses"`
}

type serverAddressDocument struct {
Version int `json:"version"`
Address string `json:"addr"`
}

func toServerAddressDocumentsByNetwork(
addresses map[string][]appcompute.ServerAddress,
) map[string][]serverAddressDocument {
documents := make(map[string][]serverAddressDocument, len(addresses))
for network, values := range addresses {
documents[network] = toServerAddressDocuments(values)
}

return documents
}

func toServerAddressDocuments(
addresses []appcompute.ServerAddress,
) []serverAddressDocument {
documents := make([]serverAddressDocument, 0, len(addresses))
for _, address := range addresses {
documents = append(documents, serverAddressDocument{
Version: address.Version,
Address: address.Address,
})
}

return documents
}
81 changes: 81 additions & 0 deletions internal/api/compute/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package compute_test

import (
"net/http/httptest"
"testing"

"github.com/JSYoo5B/SandStack/internal/api/compute"
"github.com/JSYoo5B/SandStack/internal/testhelper"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
"github.com/stretchr/testify/suite"
)

type AddressSuite struct {
suite.Suite
server *httptest.Server
}

func TestAddressSuite(t *testing.T) {
suite.Run(t, new(AddressSuite))
}

func (s *AddressSuite) SetupTest() {
s.server = httptest.NewServer(
compute.NewRouter(testhelper.DefaultConfig()),
)
}

func (s *AddressSuite) TearDownTest() {
s.server.Close()
}

func (s *AddressSuite) TestListServerAddresses() {
created := s.createServer("web")

pages, err := servers.ListAddresses(
testhelper.ServiceClient(s.server.URL+"/demo"),
created.ID,
).AllPages(s.T().Context())
s.Require().NoError(err)

addresses, err := servers.ExtractAddresses(pages)
s.Require().NoError(err)
s.Require().Contains(addresses, "private")
s.Require().Len(addresses["private"], 1)
s.Assert().Equal(4, addresses["private"][0].Version)
s.Assert().Equal("10.0.0.10", addresses["private"][0].Address)
}

func (s *AddressSuite) TestListServerAddressesByNetwork() {
created := s.createServer("web")

pages, err := servers.ListAddressesByNetwork(
testhelper.ServiceClient(s.server.URL+"/demo"),
created.ID,
"private",
).AllPages(s.T().Context())
s.Require().NoError(err)

addresses, err := servers.ExtractNetworkAddresses(pages)
s.Require().NoError(err)
s.Require().Len(addresses, 1)
s.Assert().Equal(4, addresses[0].Version)
s.Assert().Equal("10.0.0.10", addresses[0].Address)
}

func (s *AddressSuite) createServer(name string) *servers.Server {
created, err := servers.Create(
s.T().Context(),
testhelper.ServiceClient(s.server.URL+"/demo"),
servers.CreateOpts{
Name: name,
ImageRef: "img-1",
FlavorRef: "1",
},
nil,
).Extract()
s.Require().NoError(err)
s.Require().NotNil(created)

return created
}
Loading
Loading