Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e1eaf7d
feat(image): add empty image list endpoint
JSYoo5B Jun 22, 2026
412167d
feat(image): add in-memory create and list
JSYoo5B Jun 22, 2026
79272d9
feat(image): add image get and delete endpoints
JSYoo5B Jun 22, 2026
28a2a25
feat(network): add empty network list endpoint
JSYoo5B Jun 22, 2026
28e39ad
feat(network): add in-memory create and list
JSYoo5B Jun 22, 2026
063dd75
feat(network): add network get and delete endpoints
JSYoo5B Jun 22, 2026
5919a6b
feat(volume): add empty volume list endpoint
JSYoo5B Jun 22, 2026
4239bbf
feat(volume): add in-memory create and list
JSYoo5B Jun 22, 2026
bdb2565
feat(volume): add volume get and delete endpoints
JSYoo5B Jun 22, 2026
ed7a760
feat(compute): add default flavor endpoints
JSYoo5B Jun 22, 2026
ad2d281
feat(compute): add empty server list endpoints
JSYoo5B Jun 22, 2026
7ec5fda
feat(compute): add in-memory create and list
JSYoo5B Jun 22, 2026
a78a573
feat(compute): add server get and delete endpoints
JSYoo5B Jun 22, 2026
98f3ce3
feat(network): add empty subnet list endpoint
JSYoo5B Jun 22, 2026
2e29ad8
feat(network): add in-memory subnet create and list
JSYoo5B Jun 22, 2026
a903824
feat(network): add subnet get and delete endpoints
JSYoo5B Jun 22, 2026
88a34f4
feat(network): add empty port list endpoint
JSYoo5B Jun 22, 2026
fe5b1d3
feat(network): add in-memory port create and list
JSYoo5B Jun 22, 2026
9c0ebfe
feat(network): add port get and delete endpoints
JSYoo5B Jun 22, 2026
4a6cb9d
feat(volume): add default volume type endpoints
JSYoo5B Jun 22, 2026
791c424
test(api): cover phase 2 core resource flow
JSYoo5B Jun 22, 2026
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
32 changes: 32 additions & 0 deletions internal/api/compute/flavor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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) listFlavors(w http.ResponseWriter, _ *http.Request) {
respond.JSON(w, http.StatusOK, flavorListResponse{
Flavors: toFlavorDocuments(h.service.ListFlavors()),
})
}

func (h Handler) getFlavor(w http.ResponseWriter, r *http.Request) {
flavor, err := h.service.GetFlavor(chi.URLParam(r, "flavor_id"))
if errors.Is(err, appcompute.ErrFlavorNotFound) {
respond.Error(w, http.StatusNotFound, "flavor not found")
return
}
if err != nil {
respond.Error(w, http.StatusInternalServerError, "flavor lookup failed")
return
}

respond.JSON(w, http.StatusOK, flavorResponse{
Flavor: toFlavorDocument(flavor),
})
}
50 changes: 50 additions & 0 deletions internal/api/compute/flavor_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package compute

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

type flavorListResponse struct {
Flavors []flavorDocument `json:"flavors"`
}

type flavorResponse struct {
Flavor flavorDocument `json:"flavor"`
}

type flavorDocument struct {
ID string `json:"id"`
Name string `json:"name"`
RAM int `json:"ram"`
VCPUs int `json:"vcpus"`
Disk int `json:"disk"`
Swap int `json:"swap"`
RxTxFactor float64 `json:"rxtx_factor"`
IsPublic bool `json:"os-flavor-access:is_public"`
Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"`
Description string `json:"description"`
ExtraSpecs map[string]string `json:"extra_specs"`
}

func toFlavorDocuments(flavors []appcompute.Flavor) []flavorDocument {
documents := make([]flavorDocument, 0, len(flavors))
for _, flavor := range flavors {
documents = append(documents, toFlavorDocument(flavor))
}

return documents
}

func toFlavorDocument(flavor appcompute.Flavor) flavorDocument {
return flavorDocument{
ID: flavor.ID,
Name: flavor.Name,
RAM: flavor.RAM,
VCPUs: flavor.VCPUs,
Disk: flavor.Disk,
Swap: flavor.Swap,
RxTxFactor: flavor.RxTxFactor,
IsPublic: flavor.IsPublic,
Ephemeral: flavor.Ephemeral,
Description: flavor.Description,
ExtraSpecs: flavor.ExtraSpecs,
}
}
62 changes: 62 additions & 0 deletions internal/api/compute/flavor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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/flavors"
"github.com/stretchr/testify/suite"
)

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

func TestFlavorSuite(t *testing.T) {
suite.Run(t, new(FlavorSuite))
}

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

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

func (s *FlavorSuite) TestListFlavors() {
pages, err := flavors.ListDetail(
testhelper.ServiceClient(s.server.URL+"/demo"),
nil,
).AllPages(s.T().Context())
s.Require().NoError(err)

list, err := flavors.ExtractFlavors(pages)
s.Require().NoError(err)

s.Require().Len(list, 1)
s.Assert().Equal("1", list[0].ID)
s.Assert().Equal("m1.small", list[0].Name)
s.Assert().Equal(2048, list[0].RAM)
s.Assert().Equal(1, list[0].VCPUs)
s.Assert().Equal(20, list[0].Disk)
}

func (s *FlavorSuite) TestGetFlavor() {
flavor, err := flavors.Get(
s.T().Context(),
testhelper.ServiceClient(s.server.URL+"/demo"),
"1",
).Extract()
s.Require().NoError(err)
s.Require().NotNil(flavor)

s.Assert().Equal("1", flavor.ID)
s.Assert().Equal("m1.small", flavor.Name)
s.Assert().Equal(true, flavor.IsPublic)
}
17 changes: 15 additions & 2 deletions internal/api/compute/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,39 @@ package compute
import (
"net/http"

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

type Handler struct {
config config.Config
config config.Config
service *appcompute.Service
}

func NewRouter(cfg config.Config) http.Handler {
return NewHandler(cfg).Router()
}

func NewHandler(cfg config.Config) Handler {
return Handler{config: cfg}
return Handler{
config: cfg,
service: appcompute.NewService(),
}
}

func (h Handler) Router() http.Handler {
router := chi.NewRouter()
router.Get("/{project_id}", h.version)
router.Get("/{project_id}/", h.version)
router.Get("/{project_id}/flavors", h.listFlavors)
router.Get("/{project_id}/flavors/detail", h.listFlavors)
router.Get("/{project_id}/flavors/{flavor_id}", h.getFlavor)
router.Get("/{project_id}/servers", h.listServers)
router.Post("/{project_id}/servers", h.createServer)
router.Get("/{project_id}/servers/detail", h.listServers)
router.Get("/{project_id}/servers/{server_id}", h.getServer)
router.Delete("/{project_id}/servers/{server_id}", h.deleteServer)

return router
}
60 changes: 60 additions & 0 deletions internal/api/compute/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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) listServers(w http.ResponseWriter, _ *http.Request) {
respond.JSON(w, http.StatusOK, serverListResponse{
Servers: toServerDocuments(h.service.ListServers()),
})
}

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

server := h.service.CreateServer(request.createServer())
respond.JSON(w, http.StatusAccepted, serverResponse{
Server: toServerDocument(server),
})
}

func (h Handler) getServer(w http.ResponseWriter, r *http.Request) {
server, err := h.service.GetServer(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 lookup failed")
return
}

respond.JSON(w, http.StatusOK, serverResponse{
Server: toServerDocument(server),
})
}

func (h Handler) deleteServer(w http.ResponseWriter, r *http.Request) {
err := h.service.DeleteServer(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 delete failed")
return
}

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

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

type createServerRequest struct {
Server createServerDocument `json:"server"`
}

type createServerDocument struct {
Name string `json:"name"`
ImageRef string `json:"imageRef"`
FlavorRef string `json:"flavorRef"`
Metadata map[string]string `json:"metadata"`
}

func (r createServerRequest) createServer() appcompute.CreateServer {
return appcompute.CreateServer{
Name: r.Server.Name,
ImageID: r.Server.ImageRef,
FlavorID: r.Server.FlavorRef,
Metadata: r.Server.Metadata,
}
}

type serverListResponse struct {
Servers []serverDocument `json:"servers"`
}

type serverResponse struct {
Server serverDocument `json:"server"`
}

type serverDocument struct {
ID string `json:"id"`
Name string `json:"name"`
Image map[string]any `json:"image"`
Flavor map[string]any `json:"flavor"`
TenantID string `json:"tenant_id"`
UserID string `json:"user_id"`
Status string `json:"status"`
Progress int `json:"progress"`
CreatedAt string `json:"created"`
UpdatedAt string `json:"updated"`
Addresses map[string]any `json:"addresses"`
Metadata map[string]string `json:"metadata"`
Links []map[string]any `json:"links"`
}

func toServerDocuments(servers []appcompute.Server) []serverDocument {
documents := make([]serverDocument, 0, len(servers))
for _, server := range servers {
documents = append(documents, toServerDocument(server))
}

return documents
}

func toServerDocument(server appcompute.Server) serverDocument {
return serverDocument{
ID: server.ID,
Name: server.Name,
Image: map[string]any{"id": server.ImageID},
Flavor: map[string]any{"id": server.FlavorID},
TenantID: server.TenantID,
UserID: server.UserID,
Status: server.Status,
Progress: server.Progress,
CreatedAt: server.CreatedAt,
UpdatedAt: server.UpdatedAt,
Addresses: map[string]any{},
Metadata: server.Metadata,
Links: []map[string]any{
{
"href": "/servers/" + server.ID,
"rel": "self",
},
},
}
}
Loading
Loading