Skip to content

Commit 254fa50

Browse files
committed
feat(compute): add aggregate endpoints
1 parent e39fd1e commit 254fa50

13 files changed

Lines changed: 394 additions & 11 deletions

File tree

internal/api/compute/action_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func (s *ActionSuite) SetupTest() {
3030
storecompute.NewMemoryServerRepository(),
3131
storecompute.NewMemoryKeyPairRepository(),
3232
storecompute.NewMemoryServerGroupRepository(),
33+
storecompute.NewMemoryAggregateRepository(),
3334
clock.Wall(),
3435
idgen.Random(),
3536
)

internal/api/compute/aggregate.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package compute
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
"strconv"
8+
9+
"github.com/JSYoo5B/SandStack/internal/api/respond"
10+
appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"
11+
"github.com/go-chi/chi/v5"
12+
)
13+
14+
func (h Handler) listAggregates(w http.ResponseWriter, _ *http.Request) {
15+
respond.JSON(w, http.StatusOK, aggregateListResponse{
16+
Aggregates: toAggregateDocuments(h.service.ListAggregates()),
17+
})
18+
}
19+
20+
func (h Handler) createAggregate(w http.ResponseWriter, r *http.Request) {
21+
var request createAggregateRequest
22+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
23+
respond.Error(w, http.StatusBadRequest, "invalid aggregate request")
24+
return
25+
}
26+
27+
aggregate := h.service.CreateAggregate(request.createAggregate())
28+
29+
respond.JSON(w, http.StatusOK, aggregateResponse{
30+
Aggregate: toAggregateDocument(aggregate),
31+
})
32+
}
33+
34+
func (h Handler) getAggregate(w http.ResponseWriter, r *http.Request) {
35+
id, ok := aggregateID(w, r)
36+
if !ok {
37+
return
38+
}
39+
40+
aggregate, err := h.service.GetAggregate(id)
41+
if errors.Is(err, appcompute.ErrAggregateNotFound) {
42+
respond.Error(w, http.StatusNotFound, "aggregate not found")
43+
return
44+
}
45+
if err != nil {
46+
respond.Error(w, http.StatusInternalServerError, "aggregate lookup failed")
47+
return
48+
}
49+
50+
respond.JSON(w, http.StatusOK, aggregateResponse{
51+
Aggregate: toAggregateDocument(aggregate),
52+
})
53+
}
54+
55+
func (h Handler) deleteAggregate(w http.ResponseWriter, r *http.Request) {
56+
id, ok := aggregateID(w, r)
57+
if !ok {
58+
return
59+
}
60+
61+
err := h.service.DeleteAggregate(id)
62+
if errors.Is(err, appcompute.ErrAggregateNotFound) {
63+
respond.Error(w, http.StatusNotFound, "aggregate not found")
64+
return
65+
}
66+
if err != nil {
67+
respond.Error(w, http.StatusInternalServerError, "aggregate delete failed")
68+
return
69+
}
70+
71+
respond.JSON(w, http.StatusOK, map[string]any{})
72+
}
73+
74+
func aggregateID(w http.ResponseWriter, r *http.Request) (int, bool) {
75+
id, err := strconv.Atoi(chi.URLParam(r, "aggregate_id"))
76+
if err != nil {
77+
respond.Error(w, http.StatusBadRequest, "invalid aggregate id")
78+
return 0, false
79+
}
80+
81+
return id, true
82+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package compute
2+
3+
import appcompute "github.com/JSYoo5B/SandStack/internal/app/compute"
4+
5+
type createAggregateRequest struct {
6+
Aggregate createAggregateDocument `json:"aggregate"`
7+
}
8+
9+
type createAggregateDocument struct {
10+
Name string `json:"name"`
11+
AvailabilityZone string `json:"availability_zone"`
12+
}
13+
14+
type aggregateListResponse struct {
15+
Aggregates []aggregateDocument `json:"aggregates"`
16+
}
17+
18+
type aggregateResponse struct {
19+
Aggregate aggregateDocument `json:"aggregate"`
20+
}
21+
22+
type aggregateDocument struct {
23+
AvailabilityZone string `json:"availability_zone"`
24+
Hosts []string `json:"hosts"`
25+
ID int `json:"id"`
26+
Metadata map[string]string `json:"metadata"`
27+
Name string `json:"name"`
28+
CreatedAt string `json:"created_at"`
29+
UpdatedAt string `json:"updated_at"`
30+
DeletedAt *string `json:"deleted_at"`
31+
Deleted bool `json:"deleted"`
32+
UUID string `json:"uuid"`
33+
}
34+
35+
func (request createAggregateRequest) createAggregate() appcompute.CreateAggregate {
36+
return appcompute.CreateAggregate{
37+
Name: request.Aggregate.Name,
38+
AvailabilityZone: request.Aggregate.AvailabilityZone,
39+
}
40+
}
41+
42+
func toAggregateDocuments(
43+
aggregates []appcompute.Aggregate,
44+
) []aggregateDocument {
45+
documents := make([]aggregateDocument, 0, len(aggregates))
46+
for _, aggregate := range aggregates {
47+
documents = append(documents, toAggregateDocument(aggregate))
48+
}
49+
50+
return documents
51+
}
52+
53+
func toAggregateDocument(aggregate appcompute.Aggregate) aggregateDocument {
54+
var deletedAt *string
55+
if aggregate.DeletedAt != "" {
56+
deletedAt = &aggregate.DeletedAt
57+
}
58+
59+
return aggregateDocument{
60+
AvailabilityZone: aggregate.AvailabilityZone,
61+
Hosts: aggregate.Hosts,
62+
ID: aggregate.ID,
63+
Metadata: aggregate.Metadata,
64+
Name: aggregate.Name,
65+
CreatedAt: aggregate.CreatedAt,
66+
UpdatedAt: aggregate.UpdatedAt,
67+
DeletedAt: deletedAt,
68+
Deleted: aggregate.Deleted,
69+
UUID: aggregate.UUID,
70+
}
71+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package compute_test
2+
3+
import (
4+
"net/http/httptest"
5+
"testing"
6+
7+
"github.com/JSYoo5B/SandStack/internal/api/compute"
8+
"github.com/JSYoo5B/SandStack/internal/testhelper"
9+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates"
10+
"github.com/stretchr/testify/suite"
11+
)
12+
13+
type AggregateSuite struct {
14+
suite.Suite
15+
server *httptest.Server
16+
}
17+
18+
func TestAggregateSuite(t *testing.T) {
19+
suite.Run(t, new(AggregateSuite))
20+
}
21+
22+
func (s *AggregateSuite) SetupTest() {
23+
s.server = httptest.NewServer(
24+
compute.NewRouter(testhelper.DefaultConfig()),
25+
)
26+
}
27+
28+
func (s *AggregateSuite) TearDownTest() {
29+
s.server.Close()
30+
}
31+
32+
func (s *AggregateSuite) TestCreateListGetAndDeleteAggregate() {
33+
client := testhelper.ServiceClient(s.server.URL + "/demo")
34+
created, err := aggregates.Create(
35+
s.T().Context(),
36+
client,
37+
aggregates.CreateOpts{
38+
Name: "az-one",
39+
AvailabilityZone: "nova",
40+
},
41+
).Extract()
42+
s.Require().NoError(err)
43+
s.Require().NotNil(created)
44+
45+
pages, err := aggregates.List(client).AllPages(s.T().Context())
46+
s.Require().NoError(err)
47+
listed, err := aggregates.ExtractAggregates(pages)
48+
s.Require().NoError(err)
49+
found, err := aggregates.Get(
50+
s.T().Context(),
51+
client,
52+
created.ID,
53+
).Extract()
54+
s.Require().NoError(err)
55+
s.Require().NotNil(found)
56+
57+
s.Assert().Equal(1, created.ID)
58+
s.Assert().Equal("az-one", created.Name)
59+
s.Assert().Equal("nova", created.AvailabilityZone)
60+
s.Require().Len(listed, 1)
61+
s.Assert().Equal(created.ID, listed[0].ID)
62+
s.Assert().Equal(created.ID, found.ID)
63+
64+
err = aggregates.Delete(
65+
s.T().Context(),
66+
client,
67+
created.ID,
68+
).ExtractErr()
69+
s.Require().NoError(err)
70+
}

internal/api/compute/metadata_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (s *MetadataSuite) SetupTest() {
2929
storecompute.NewMemoryServerRepository(),
3030
storecompute.NewMemoryKeyPairRepository(),
3131
storecompute.NewMemoryServerGroupRepository(),
32+
storecompute.NewMemoryAggregateRepository(),
3233
clock.Wall(),
3334
idgen.Random(),
3435
)

internal/api/compute/router.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewHandler(cfg config.Config) Handler {
3434
storecompute.NewMemoryServerRepository(),
3535
storecompute.NewMemoryKeyPairRepository(),
3636
storecompute.NewMemoryServerGroupRepository(),
37+
storecompute.NewMemoryAggregateRepository(),
3738
clock.Wall(),
3839
idgen.Random(),
3940
),
@@ -61,6 +62,10 @@ func (h Handler) Router() http.Handler {
6162
router.Get("/{project_id}/os-hypervisors/detail", h.listHypervisors)
6263
router.Get("/{project_id}/os-hypervisors/statistics", h.getHypervisorStatistics)
6364
router.Get("/{project_id}/os-hypervisors/{hypervisor_id}", h.getHypervisor)
65+
router.Get("/{project_id}/os-aggregates", h.listAggregates)
66+
router.Post("/{project_id}/os-aggregates", h.createAggregate)
67+
router.Get("/{project_id}/os-aggregates/{aggregate_id}", h.getAggregate)
68+
router.Delete("/{project_id}/os-aggregates/{aggregate_id}", h.deleteAggregate)
6469
router.Get("/{project_id}/flavors", h.listFlavors)
6570
router.Get("/{project_id}/flavors/detail", h.listFlavors)
6671
router.Get("/{project_id}/flavors/{flavor_id}", h.getFlavor)

internal/api/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func NewRouter(cfg config.Config) http.Handler {
4848
storecompute.NewMemoryServerRepository(),
4949
storecompute.NewMemoryKeyPairRepository(),
5050
storecompute.NewMemoryServerGroupRepository(),
51+
storecompute.NewMemoryAggregateRepository(),
5152
clock.Wall(),
5253
idgen.Random(),
5354
)

internal/app/compute/aggregate.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package compute
2+
3+
import "errors"
4+
5+
var ErrAggregateNotFound = errors.New("aggregate not found")
6+
7+
const aggregateTimestampFormat = "2006-01-02T15:04:05.000000"
8+
9+
func (s *Service) CreateAggregate(input CreateAggregate) Aggregate {
10+
now := s.clock.Now().UTC().Format(aggregateTimestampFormat)
11+
id := len(s.aggregateRepository.List()) + 1
12+
13+
return s.aggregateRepository.Create(Aggregate{
14+
ID: id,
15+
Name: input.Name,
16+
AvailabilityZone: input.AvailabilityZone,
17+
Hosts: []string{},
18+
Metadata: map[string]string{},
19+
CreatedAt: now,
20+
UpdatedAt: now,
21+
UUID: "aggregate-" + s.idGen.Hex(16),
22+
})
23+
}
24+
25+
func (s *Service) ListAggregates() []Aggregate {
26+
return s.aggregateRepository.List()
27+
}
28+
29+
func (s *Service) GetAggregate(id int) (Aggregate, error) {
30+
return s.aggregateRepository.Get(id)
31+
}
32+
33+
func (s *Service) DeleteAggregate(id int) error {
34+
return s.aggregateRepository.Delete(id)
35+
}

internal/app/compute/repository.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ type ServerGroupRepository interface {
2424
Delete(id string) error
2525
Reset()
2626
}
27+
28+
type AggregateRepository interface {
29+
Create(aggregate Aggregate) Aggregate
30+
List() []Aggregate
31+
Get(id int) (Aggregate, error)
32+
Delete(id int) error
33+
Reset()
34+
}

internal/app/compute/server_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func (s *ServerSuite) TestCreateServerUsesInjectedClock() {
2525
storecompute.NewMemoryServerRepository(),
2626
storecompute.NewMemoryKeyPairRepository(),
2727
storecompute.NewMemoryServerGroupRepository(),
28+
storecompute.NewMemoryAggregateRepository(),
2829
clock.Fixed(now),
2930
idgen.Random(),
3031
)
@@ -44,6 +45,7 @@ func (s *ServerSuite) TestCreateServerUsesInjectedIDGenerator() {
4445
storecompute.NewMemoryServerRepository(),
4546
storecompute.NewMemoryKeyPairRepository(),
4647
storecompute.NewMemoryServerGroupRepository(),
48+
storecompute.NewMemoryAggregateRepository(),
4749
clock.Fixed(time.Time{}),
4850
idgen.Fixed("server-id"),
4951
)
@@ -63,6 +65,7 @@ func (s *ServerSuite) TestGetServerActivatesCreatedServer() {
6365
storecompute.NewMemoryServerRepository(),
6466
storecompute.NewMemoryKeyPairRepository(),
6567
storecompute.NewMemoryServerGroupRepository(),
68+
storecompute.NewMemoryAggregateRepository(),
6669
clock.Fixed(now),
6770
idgen.Fixed("server-id"),
6871
)
@@ -86,6 +89,7 @@ func (s *ServerSuite) TestResetClearsServers() {
8689
storecompute.NewMemoryServerRepository(),
8790
storecompute.NewMemoryKeyPairRepository(),
8891
storecompute.NewMemoryServerGroupRepository(),
92+
storecompute.NewMemoryAggregateRepository(),
8993
clock.Fixed(time.Time{}),
9094
idgen.Fixed("server-id"),
9195
)

0 commit comments

Comments
 (0)