Skip to content
42 changes: 42 additions & 0 deletions internal/api/placement/aggregate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package placement

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

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

func (h Handler) getAggregates(w http.ResponseWriter, r *http.Request) {
aggregates, err := h.service.GetAggregates(
chi.URLParam(r, "resource_provider_uuid"),
)
if handlePlacementError(w, err, "aggregate lookup failed") {
return
}

respond.JSON(w, http.StatusOK, toAggregatesDocument(aggregates))
}

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

aggregates, err := h.service.UpdateAggregates(
chi.URLParam(r, "resource_provider_uuid"),
appplacement.UpdateAggregates{
ResourceProviderGeneration: request.ResourceProviderGeneration,
Aggregates: request.Aggregates,
},
)
if handlePlacementError(w, err, "aggregate update failed") {
return
}

respond.JSON(w, http.StatusOK, toAggregatesDocument(aggregates))
}
17 changes: 17 additions & 0 deletions internal/api/placement/aggregate_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package placement

import appplacement "github.com/JSYoo5B/SandStack/internal/app/placement"

type aggregatesDocument struct {
ResourceProviderGeneration *int `json:"resource_provider_generation,omitempty"`
Aggregates []string `json:"aggregates"`
}

func toAggregatesDocument(
aggregates appplacement.Aggregates,
) aggregatesDocument {
return aggregatesDocument{
ResourceProviderGeneration: aggregates.ResourceProviderGeneration,
Aggregates: aggregates.Aggregates,
}
}
80 changes: 80 additions & 0 deletions internal/api/placement/aggregate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package placement_test

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

"github.com/JSYoo5B/SandStack/internal/api/placement"
"github.com/JSYoo5B/SandStack/internal/testhelper"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders"
"github.com/stretchr/testify/suite"
)

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

func TestAggregateSuite(t *testing.T) {
suite.Run(t, new(AggregateSuite))
}

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

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

func (s *AggregateSuite) TestUpdateAndGetAggregates() {
client := testhelper.ServiceClient(s.server.URL)
provider := s.createResourceProvider(client)

updated, err := resourceproviders.UpdateAggregates(
context.Background(),
client,
provider.UUID,
resourceproviders.UpdateAggregatesOpts{
ResourceProviderGeneration: &provider.Generation,
Aggregates: []string{
"aggregate-1",
"aggregate-2",
},
},
).Extract()
s.Require().NoError(err)

found, err := resourceproviders.GetAggregates(
context.Background(),
client,
provider.UUID,
).Extract()
s.Require().NoError(err)

s.Assert().ElementsMatch(
[]string{"aggregate-1", "aggregate-2"},
updated.Aggregates,
)
s.Assert().ElementsMatch(updated.Aggregates, found.Aggregates)
}

func (s *AggregateSuite) createResourceProvider(
client *gophercloud.ServiceClient,
) *resourceproviders.ResourceProvider {
provider, err := resourceproviders.Create(
context.Background(),
client,
resourceproviders.CreateOpts{
Name: "compute-1",
UUID: "resource-provider-1",
},
).Extract()
s.Require().NoError(err)

return provider
}
19 changes: 19 additions & 0 deletions internal/api/placement/allocation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package placement

import (
"net/http"

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

func (h Handler) getAllocations(w http.ResponseWriter, r *http.Request) {
allocations, err := h.service.GetAllocations(
chi.URLParam(r, "resource_provider_uuid"),
)
if handlePlacementError(w, err, "allocation lookup failed") {
return
}

respond.JSON(w, http.StatusOK, toAllocationsDocument(allocations))
}
43 changes: 43 additions & 0 deletions internal/api/placement/allocation_candidate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package placement

import (
"net/http"
"strconv"
"strings"

"github.com/JSYoo5B/SandStack/internal/api/respond"
appplacement "github.com/JSYoo5B/SandStack/internal/app/placement"
)

func (h Handler) listAllocationCandidates(
w http.ResponseWriter,
r *http.Request,
) {
candidates := h.service.ListAllocationCandidates(
appplacement.AllocationCandidateQuery{
Resources: parseRequestedResources(
r.URL.Query().Get("resources"),
),
},
)

respond.JSON(w, http.StatusOK, toAllocationCandidatesDocument(candidates))
}

func parseRequestedResources(value string) map[string]int {
resources := map[string]int{}
for _, part := range strings.Split(value, ",") {
resourceClass, amount, ok := strings.Cut(part, ":")
if !ok {
continue
}

parsedAmount, err := strconv.Atoi(amount)
if err != nil {
continue
}
resources[resourceClass] = parsedAmount
}

return resources
}
88 changes: 88 additions & 0 deletions internal/api/placement/allocation_candidate_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package placement

import appplacement "github.com/JSYoo5B/SandStack/internal/app/placement"

type allocationCandidatesDocument struct {
AllocationRequests []allocationRequestDocument `json:"allocation_requests"`
ProviderSummaries map[string]providerSummaryDocument `json:"provider_summaries"`
}

type allocationRequestDocument struct {
Allocations map[string]allocationDocument `json:"allocations"`
}

type providerSummaryDocument struct {
Resources map[string]providerSummaryResourceDocument `json:"resources"`
Traits []string `json:"traits,omitempty"`
ParentProviderUUID string `json:"parent_provider_uuid,omitempty"`
RootProviderUUID string `json:"root_provider_uuid,omitempty"`
}

type providerSummaryResourceDocument struct {
Capacity int `json:"capacity"`
Used int `json:"used"`
}

func toAllocationCandidatesDocument(
candidates appplacement.AllocationCandidates,
) allocationCandidatesDocument {
return allocationCandidatesDocument{
AllocationRequests: toAllocationRequestDocuments(
candidates.AllocationRequests,
),
ProviderSummaries: toProviderSummaryDocuments(
candidates.ProviderSummaries,
),
}
}

func toAllocationRequestDocuments(
requests []appplacement.AllocationRequest,
) []allocationRequestDocument {
documents := make([]allocationRequestDocument, 0, len(requests))
for _, request := range requests {
allocations := map[string]allocationDocument{}
for providerUUID, allocation := range request.Allocations {
allocations[providerUUID] = allocationDocument{
Resources: allocation.Resources,
}
}
documents = append(documents, allocationRequestDocument{
Allocations: allocations,
})
}

return documents
}

func toProviderSummaryDocuments(
summaries map[string]appplacement.ProviderSummary,
) map[string]providerSummaryDocument {
documents := map[string]providerSummaryDocument{}
for providerUUID, summary := range summaries {
documents[providerUUID] = providerSummaryDocument{
Resources: toProviderSummaryResourceDocuments(
summary.Resources,
),
Traits: summary.Traits,
ParentProviderUUID: summary.ParentProviderUUID,
RootProviderUUID: summary.RootProviderUUID,
}
}

return documents
}

func toProviderSummaryResourceDocuments(
resources map[string]appplacement.ProviderSummaryResource,
) map[string]providerSummaryResourceDocument {
documents := map[string]providerSummaryResourceDocument{}
for resourceClass, resource := range resources {
documents[resourceClass] = providerSummaryResourceDocument{
Capacity: resource.Capacity,
Used: resource.Used,
}
}

return documents
}
98 changes: 98 additions & 0 deletions internal/api/placement/allocation_candidate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package placement_test

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

"github.com/JSYoo5B/SandStack/internal/api/placement"
"github.com/JSYoo5B/SandStack/internal/testhelper"
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/allocationcandidates"
"github.com/gophercloud/gophercloud/v2/openstack/placement/v1/resourceproviders"
"github.com/stretchr/testify/suite"
)

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

func TestAllocationCandidateSuite(t *testing.T) {
suite.Run(t, new(AllocationCandidateSuite))
}

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

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

func (s *AllocationCandidateSuite) TestListAllocationCandidates() {
client := testhelper.ServiceClient(s.server.URL)
provider := s.createResourceProvider(client)
s.updateInventory(client, provider.UUID)

pages, err := allocationcandidates.List(
client,
allocationcandidates.ListOpts{
Resources: "VCPU:2",
},
).AllPages(context.Background())
s.Require().NoError(err)

candidates, err := allocationcandidates.ExtractAllocationCandidates(pages)
s.Require().NoError(err)

s.Require().Len(candidates.AllocationRequests, 1)
allocation := candidates.AllocationRequests[0].Allocations[provider.UUID]
s.Assert().Equal(2, allocation.Resources["VCPU"])
s.Require().Contains(candidates.ProviderSummaries, provider.UUID)
s.Assert().Equal(
64,
candidates.ProviderSummaries[provider.UUID].Resources["VCPU"].Capacity,
)
}

func (s *AllocationCandidateSuite) createResourceProvider(
client *gophercloud.ServiceClient,
) *resourceproviders.ResourceProvider {
provider, err := resourceproviders.Create(
context.Background(),
client,
resourceproviders.CreateOpts{
Name: "compute-1",
UUID: "resource-provider-1",
},
).Extract()
s.Require().NoError(err)

return provider
}

func (s *AllocationCandidateSuite) updateInventory(
client *gophercloud.ServiceClient,
providerUUID string,
) {
_, err := resourceproviders.UpdateInventory(
context.Background(),
client,
providerUUID,
"VCPU",
resourceproviders.UpdateInventoryOpts{
Inventory: resourceproviders.Inventory{
AllocationRatio: 16.0,
MaxUnit: 4,
MinUnit: 1,
Reserved: 0,
StepSize: 1,
Total: 4,
},
},
).Extract()
s.Require().NoError(err)
}
Loading
Loading