Skip to content

Commit

Permalink
CCX-5102 AZ Allocation
Browse files Browse the repository at this point in the history
Availability Zones are allocated fairly (on creation and resize) if not specified or number of nodes exceeds those specified
  • Loading branch information
fluxynet committed Dec 31, 2024
1 parent 404269e commit 7f746fc
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 47 deletions.
39 changes: 37 additions & 2 deletions internal/ccx/api/content_instancesizes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package api

import (
"context"
"fmt"

"github.com/severalnines/terraform-provider-ccx/internal/ccx"
)

type deployWizardResponse struct {
type instanceSizesResponse struct {
Instance struct {
InstanceSizes map[string][]ccx.InstanceSize `json:"instance_sizes"`
} `json:"instance"`
}

func (svc *ContentService) InstanceSizes(ctx context.Context) (map[string][]ccx.InstanceSize, error) {
var rs deployWizardResponse
var rs instanceSizesResponse

err := svc.client.Get(ctx, "/api/content/api/v1/deploy-wizard", &rs)
if err != nil {
Expand All @@ -22,3 +23,37 @@ func (svc *ContentService) InstanceSizes(ctx context.Context) (map[string][]ccx.

return rs.Instance.InstanceSizes, nil
}

type availabilityZonesResponse struct {
Network struct {
AvailabilityZones map[string]map[string][]struct {
Code string `json:"code"`
} `json:"availability_zones"`
} `json:"network"`
}

func (svc *ContentService) AvailabilityZones(ctx context.Context, provider, region string) ([]string, error) {
var rs availabilityZonesResponse

err := svc.client.Get(ctx, "/api/content/api/v1/deploy-wizard", &rs)
if err != nil {
return nil, err
}

p, ok := rs.Network.AvailabilityZones[provider]
if !ok {
return nil, fmt.Errorf("no availability zones found for provider %q", provider)
}

r, ok := p[region]
if !ok {
return nil, fmt.Errorf("no availability zones found for provider %q in region %q", provider, region)
}

ls := make([]string, 0, len(r))
for _, az := range r {
ls = append(ls, az.Code)
}

return ls, nil
}
12 changes: 7 additions & 5 deletions internal/ccx/api/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import (
)

type DatastoreService struct {
client HttpClient
jobs jobService
client HttpClient
jobs jobService
contentSvc ccx.ContentService
}

var _ ccx.DatastoreService = (*DatastoreService)(nil)

// Datastores creates a new datastores DatastoreService
func Datastores(client HttpClient, timeout time.Duration) (*DatastoreService, error) {
func Datastores(client HttpClient, timeout time.Duration, contentSvc ccx.ContentService) (*DatastoreService, error) {
j := newJobs(client, timeout)

c := DatastoreService{
client: client,
jobs: j,
client: client,
jobs: j,
contentSvc: contentSvc,
}

return &c, nil
Expand Down
39 changes: 39 additions & 0 deletions internal/ccx/api/datastore_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ func createRequestFromDatastore(c ccx.Datastore) createStoreRequest {
}
}

func allocateAzs(allAzs, existing []string, n int) []string {
if n <= 0 {
return nil
}

m := make(map[string]int, len(allAzs))

for _, a := range allAzs {
m[a] = 0
}

for _, e := range existing {
if _, ok := m[e]; ok { // skip those not in the list, might be older ones no longer available
m[e] += 1
}
}

azs := make([]lib.CountedItem, 0, len(m))
for name, count := range m {
azs = append(azs, lib.CountedItem{
Name: name,
Count: count,
})
}

ls := lib.AllocateN(azs, n)

return ls
}

type datastoreResponse struct {
UUID string `json:"uuid"`
Name string `json:"cluster_name"`
Expand All @@ -118,6 +148,15 @@ type datastoreResponse struct {
func (svc *DatastoreService) Create(ctx context.Context, c ccx.Datastore) (*ccx.Datastore, error) {
cr := createRequestFromDatastore(c)

if n, h := len(c.AvailabilityZones), int(c.Size); c.NetworkType == "public" && n < h { // allocate AZs if public and need is less than have
allAzs, err := svc.contentSvc.AvailabilityZones(ctx, c.CloudProvider, c.CloudRegion)
if err != nil {
return nil, fmt.Errorf("allocating availability zones: %w: %w", ccx.CreateFailedErr, err)
}

c.AvailabilityZones = allocateAzs(allAzs, nil, h-n)
}

res, err := svc.client.Do(ctx, http.MethodPost, "/api/prov/api/v2/cluster", cr)
if err != nil {
return nil, errors.Join(ccx.RequestSendingErr, err)
Expand Down
44 changes: 25 additions & 19 deletions internal/ccx/api/datastore_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ func (svc *DatastoreService) resize(ctx context.Context, old, next ccx.Datastore
return false, nil
}

adding := old.Size < next.Size

if adding {
if n, h := len(next.AvailabilityZones), int(next.Size); next.NetworkType == "public" && n < h { // allocate AZs if public and need is less than have
allAzs, err := svc.contentSvc.AvailabilityZones(ctx, next.CloudProvider, next.CloudRegion)
if err != nil {
return false, fmt.Errorf("allocating availability zones: %w: %w", ccx.CreateFailedErr, err)
}

next.AvailabilityZones = allocateAzs(allAzs, nil, h-n)
}
}

res, err := svc.client.Do(ctx, http.MethodPatch, "/api/prov/api/v2/cluster/"+next.ID, ur)
if err != nil {
return false, errors.Join(ccx.RequestSendingErr, err)
Expand All @@ -108,14 +121,10 @@ func (svc *DatastoreService) resize(ctx context.Context, old, next ccx.Datastore
}

var jt jobType

if old.Size > next.Size {
jt = removeNodeJob
} else if old.Size < next.Size {
if adding {
jt = addNodeJob
} else {
// should not be here
return false, nil
jt = removeNodeJob
}

status, err := svc.jobs.Await(ctx, old.ID, jt)
Expand All @@ -129,10 +138,7 @@ func (svc *DatastoreService) resize(ctx context.Context, old, next ccx.Datastore
}

func (svc *DatastoreService) updateSizeRequest(old, next ccx.Datastore) (updateRequest, bool) {
var (
ur updateRequest
ok bool
)
var ur updateRequest

if old.Size > next.Size { // remove the oldest non-primary nodes
ids, err := oldestRemovableNodeIds(old.Hosts, int(old.Size-next.Size))
Expand All @@ -141,18 +147,18 @@ func (svc *DatastoreService) updateSizeRequest(old, next ccx.Datastore) (updateR
}

ur.Remove = &removeHosts{HostIDs: ids}
ok = true
} else if old.Size < next.Size { // add new hosts based on newest node spec
specs, err := newestNodeSpecs(old.Hosts, int(next.Size-old.Size))
if err != nil {
return ur, false
}
return ur, true
}

ur.Add = &addHosts{Specs: specs}
ok = true
// add new hosts based on newest node spec
specs, err := newestNodeSpecs(old.Hosts, int(next.Size-old.Size))
if err != nil {
return ur, false
}

return ur, ok
ur.Add = &addHosts{Specs: specs}

return ur, true
}

func (svc *DatastoreService) updateRequest(old, next ccx.Datastore) (updateRequest, bool) {
Expand Down
60 changes: 60 additions & 0 deletions internal/ccx/mocks/content_service_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/ccx/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,5 @@ type InstanceSize struct {

type ContentService interface {
InstanceSizes(ctx context.Context) (map[string][]InstanceSize, error)
AvailabilityZones(ctx context.Context, provider, region string) ([]string, error)
}
59 changes: 59 additions & 0 deletions internal/lib/allocate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package lib

import (
"math"
"slices"
"strings"
)

type CountedItem struct {
Name string
Count int
}

func AllocateN(entries []CountedItem, n int) []string {
total := len(entries)
if n <= 0 || total == 0 {
return nil
}

sum := n
for _, e := range entries {
sum += e.Count
}

average := int(math.Ceil(float64(sum) / float64(total)))
ls := make([]string, 0, n)

slices.SortStableFunc(entries, func(a, b CountedItem) int { // allocation to start with items having the lowest counts
if a.Count == b.Count {
return strings.Compare(a.Name, b.Name)
}

return a.Count - b.Count
})

for _, e := range entries {
if e.Count > average { // skip the ones with more than average, they get nothing
continue
}

w := average - e.Count // how many more to add to reach average

if w > n { // do not exceed what we need
w = n
}

n -= w

for i := 0; i < w; i++ {
ls = append(ls, e.Name)
}

if n == 0 { // we do not need more
return ls
}
}

return ls
}
Loading

0 comments on commit 7f746fc

Please sign in to comment.