Skip to content

Commit 57d5719

Browse files
committed
ECOPROJECT-3992 | feat: sizer - support hosted control plane nodes
Signed-off-by: Ronen Avraham <ravraham@redhat.com>
1 parent a1f1b26 commit 57d5719

File tree

9 files changed

+223
-122
lines changed

9 files changed

+223
-122
lines changed

api/v1alpha1/openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,10 @@ components:
17651765
description: Number of control plane nodes (1 for single node, 3 for HA)
17661766
x-oapi-codegen-extra-tags:
17671767
validate: "omitempty,oneof=1 3"
1768+
hostedControlPlane:
1769+
type: boolean
1770+
default: false
1771+
description: When true, control plane is hosted elsewhere; only worker nodes are sized.
17681772
required:
17691773
- clusterId
17701774
- cpuOverCommitRatio

api/v1alpha1/spec.gen.go

Lines changed: 111 additions & 111 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha1/types.gen.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/handlers/v1alpha1/mappers/inbound.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ func ClusterRequirementsRequestToForm(apiReq v1alpha1.ClusterRequirementsRequest
152152
} else {
153153
form.ControlPlaneNodeCount = int(v1alpha1.N3)
154154
}
155+
if apiReq.HostedControlPlane != nil {
156+
form.HostedControlPlane = *apiReq.HostedControlPlane
157+
}
155158
return form
156159
}
157160

internal/handlers/v1alpha1/sizer.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,16 @@ func (h *ServiceHandler) CalculateAssessmentClusterRequirements(ctx context.Cont
5656
}
5757

5858
// Validate controlPlaneNodeCount (API enum handles this in production, but tests bypass middleware)
59-
if request.Body.ControlPlaneNodeCount != nil {
60-
count := int(*request.Body.ControlPlaneNodeCount)
61-
if count != 1 && count != 3 {
62-
logger.Error(fmt.Errorf("invalid controlPlaneNodeCount: %d", count)).Log()
63-
return server.CalculateAssessmentClusterRequirements400JSONResponse{
64-
Message: fmt.Sprintf("invalid controlPlaneNodeCount: %d", count),
65-
}, nil
59+
// Skip when hosted control plane; those fields are ignored.
60+
if request.Body.HostedControlPlane == nil || !*request.Body.HostedControlPlane {
61+
if request.Body.ControlPlaneNodeCount != nil {
62+
count := int(*request.Body.ControlPlaneNodeCount)
63+
if count != 1 && count != 3 {
64+
logger.Error(fmt.Errorf("invalid controlPlaneNodeCount: %d", count)).Log()
65+
return server.CalculateAssessmentClusterRequirements400JSONResponse{
66+
Message: fmt.Sprintf("invalid controlPlaneNodeCount: %d", count),
67+
}, nil
68+
}
6669
}
6770
}
6871

internal/handlers/v1alpha1/sizer_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,44 @@ var _ = Describe("sizer handler", func() {
354354
Expect(successResp.ResourceConsumption.Memory).To(Equal(200.0))
355355
})
356356

357+
It("returns 200 with hostedControlPlane true and zero control plane nodes in response", func() {
358+
hostedTrue := true
359+
invalidCount := api.ClusterRequirementsRequestControlPlaneNodeCount(2)
360+
request := &api.ClusterRequirementsRequest{
361+
ClusterId: clusterID,
362+
CpuOverCommitRatio: api.CpuOneToFour,
363+
MemoryOverCommitRatio: api.MemoryOneToTwo,
364+
WorkerNodeCPU: 8,
365+
WorkerNodeMemory: 16,
366+
HostedControlPlane: &hostedTrue,
367+
ControlPlaneNodeCount: &invalidCount,
368+
}
369+
370+
mockStore.assessments[assessmentID] = createTestAssessment(assessmentID, user.Username, user.Organization, clusterID)
371+
testServer = createTestSizerServer(createTestSizerResponse(4, 4, 0, 40, 80), http.StatusOK, false)
372+
sizerClient = client.NewSizerClient(testServer.URL, 5*time.Second)
373+
handler = handlers.NewServiceHandler(
374+
nil,
375+
service.NewAssessmentService(mockStore, nil),
376+
nil,
377+
service.NewSizerService(sizerClient, mockStore),
378+
nil,
379+
)
380+
381+
resp, err := handler.CalculateAssessmentClusterRequirements(ctx, server.CalculateAssessmentClusterRequirementsRequestObject{
382+
Id: assessmentID,
383+
Body: request,
384+
})
385+
386+
Expect(err).To(BeNil())
387+
Expect(resp).NotTo(BeNil())
388+
successResp, ok := resp.(server.CalculateAssessmentClusterRequirements200JSONResponse)
389+
Expect(ok).To(BeTrue())
390+
Expect(successResp.ClusterSizing.ControlPlaneNodes).To(Equal(0))
391+
Expect(successResp.ClusterSizing.WorkerNodes).To(Equal(6))
392+
Expect(successResp.ClusterSizing.TotalNodes).To(Equal(6))
393+
})
394+
357395
intPtr := func(v int) *int { return &v }
358396
cases := []struct {
359397
name string

internal/service/mappers/inbound.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ type ClusterRequirementsRequestForm struct {
235235
ControlPlaneCPU int
236236
ControlPlaneMemory int
237237
ControlPlaneNodeCount int
238+
HostedControlPlane bool
238239
}
239240

240241
type ClusterRequirementsResponseForm struct {

internal/service/sizer.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ func (s *SizerService) CalculateClusterRequirements(
173173
return nil, NewErrInvalidClusterInventory(req.ClusterID, "cluster has no VMs or no CPU/Memory resources and cannot be used for migration planning")
174174
}
175175

176-
includeControlPlane := true
176+
includeControlPlane := !req.HostedControlPlane
177+
effectiveCPNodeCount := effectiveControlPlaneNodeCount(req)
177178
controlPlaneSchedulable := req.ControlPlaneSchedulable
178179

179180
controlPlaneCPU := req.ControlPlaneCPU
@@ -201,7 +202,8 @@ func (s *SizerService) CalculateClusterRequirements(
201202
WithString("worker_node_effective_cpu", fmt.Sprintf("%.2f", effectiveCPU)).
202203
WithInt("worker_node_memory", req.WorkerNodeMemory).
203204
WithBool("control_plane_schedulable", controlPlaneSchedulable).
204-
WithInt("control_plane_node_count", req.ControlPlaneNodeCount).
205+
WithBool("hosted_control_plane", req.HostedControlPlane).
206+
WithInt("control_plane_node_count", effectiveCPNodeCount).
205207
Build()
206208

207209
// Use effective CPU for all calculations
@@ -255,7 +257,7 @@ func (s *SizerService) CalculateClusterRequirements(
255257
controlPlaneSchedulable,
256258
controlPlaneCPU,
257259
controlPlaneMemory,
258-
req.ControlPlaneNodeCount,
260+
effectiveCPNodeCount,
259261
)
260262

261263
// Call sizer service
@@ -268,7 +270,7 @@ func (s *SizerService) CalculateClusterRequirements(
268270
return nil, fmt.Errorf("sizer service returned empty response")
269271
}
270272

271-
transformed := s.transformSizerResponse(sizerResponse, req.ControlPlaneNodeCount)
273+
transformed := s.transformSizerResponse(sizerResponse, effectiveCPNodeCount)
272274

273275
if transformed.ClusterSizing.TotalNodes > MaxNodeCount {
274276
minNodeCPU, minNodeMemory := s.calculateMinimumNodeSize(
@@ -506,6 +508,13 @@ func (s *SizerService) formatNodeSizeError(workerCPU, workerMemory, inventoryCPU
506508
return NewErrInvalidRequest(message)
507509
}
508510

511+
func effectiveControlPlaneNodeCount(req *mappers.ClusterRequirementsRequestForm) int {
512+
if req.HostedControlPlane {
513+
return 0
514+
}
515+
return req.ControlPlaneNodeCount
516+
}
517+
509518
// buildSizerPayload transforms batched services into sizer API format
510519
func (s *SizerService) buildSizerPayload(
511520
services []BatchedService,

internal/service/sizer_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,46 @@ var _ = Describe("sizer service", func() {
296296
Expect(result).NotTo(BeNil())
297297
})
298298

299+
It("successfully handles hosted control plane (worker nodes only)", func() {
300+
request.HostedControlPlane = true
301+
assessment := createTestAssessment(assessmentID, clusterID, 10, 40, 80)
302+
mockStore.assessments[assessmentID] = assessment
303+
304+
var sizerPayload client.SizerRequest
305+
testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
306+
if r.URL.Path == "/api/v1/size/custom" && r.Method == http.MethodPost {
307+
Expect(json.NewDecoder(r.Body).Decode(&sizerPayload)).To(Succeed())
308+
}
309+
if r.URL.Path == "/health" {
310+
w.WriteHeader(http.StatusOK)
311+
return
312+
}
313+
if r.URL.Path == "/api/v1/size/custom" {
314+
w.Header().Set("Content-Type", "application/json")
315+
w.WriteHeader(http.StatusOK)
316+
_ = json.NewEncoder(w).Encode(createTestSizerResponse(4, 4, 0, 40, 80))
317+
return
318+
}
319+
w.WriteHeader(http.StatusNotFound)
320+
}))
321+
sizerClient = client.NewSizerClient(testServer.URL, 5*time.Second)
322+
sizerService = service.NewSizerService(sizerClient, mockStore)
323+
324+
result, err := sizerService.CalculateClusterRequirements(ctx, assessmentID, request)
325+
326+
Expect(err).To(BeNil())
327+
Expect(result).NotTo(BeNil())
328+
Expect(result.ClusterSizing.ControlPlaneNodes).To(Equal(0))
329+
Expect(result.ClusterSizing.WorkerNodes).To(Equal(6))
330+
Expect(result.ClusterSizing.TotalNodes).To(Equal(6))
331+
Expect(result.ClusterSizing.FailoverNodes).To(Equal(2))
332+
333+
Expect(sizerPayload.MachineSets).To(HaveLen(1))
334+
Expect(sizerPayload.MachineSets[0].Name).To(Equal("worker"))
335+
Expect(sizerPayload.Workloads).To(HaveLen(1))
336+
Expect(sizerPayload.Workloads[0].Name).To(Equal("vm-workload"))
337+
})
338+
299339
It("successfully handles different over-commit ratios", func() {
300340
cpuRatios := []string{"1:1", "1:2", "1:4", "1:6"}
301341
memoryRatios := []string{"1:1", "1:2", "1:4"}

0 commit comments

Comments
 (0)