Skip to content

Commit 37720cc

Browse files
ronenavopenshift-merge-bot[bot]
authored andcommitted
ECOPROJECT-3992 | feat: sizer - support hosted control plane nodes
Signed-off-by: Ronen Avraham <ravraham@redhat.com>
1 parent 3eb30ab commit 37720cc

File tree

9 files changed

+366
-144
lines changed

9 files changed

+366
-144
lines changed

api/v1alpha1/openapi.yaml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,27 +1752,26 @@ components:
17521752
validate: "omitempty,min=2"
17531753
controlPlaneSchedulable:
17541754
type: boolean
1755-
default: false
1756-
description: Allow workload scheduling on control plane nodes
1755+
description: "Allow workload scheduling on control plane nodes (default: false)"
17571756
controlPlaneCPU:
17581757
type: integer
1759-
default: 6
17601758
description: "CPU cores per control plane node (default: 6)"
17611759
x-oapi-codegen-extra-tags:
17621760
validate: "omitempty,min=2,max=200"
17631761
controlPlaneMemory:
17641762
type: integer
1765-
default: 16
1766-
description: "Memory (GB) per control plane node (default: 16)"
1763+
description: "Memory in GB per control plane node (default: 16)"
17671764
x-oapi-codegen-extra-tags:
17681765
validate: "omitempty,min=4,max=512"
17691766
controlPlaneNodeCount:
17701767
type: integer
17711768
enum: [1, 3]
1772-
default: 3
1773-
description: Number of control plane nodes (1 for single node, 3 for HA)
1769+
description: "Number of control plane nodes: 1 or 3 (default: 3)"
17741770
x-oapi-codegen-extra-tags:
17751771
validate: "omitempty,oneof=1 3"
1772+
hostedControlPlane:
1773+
type: boolean
1774+
description: "If true, control plane is hosted externally. Incompatible with control plane fields (default: false)"
17761775
required:
17771776
- clusterId
17781777
- cpuOverCommitRatio

api/v1alpha1/spec.gen.go

Lines changed: 98 additions & 98 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: 7 additions & 4 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: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -120,38 +120,61 @@ func ClusterRequirementsRequestToForm(apiReq v1alpha1.ClusterRequirementsRequest
120120
// Default control plane node resources align with OVF sizer defaults and meet
121121
// Assisted Installer prerequisites for control plane nodes.
122122
const (
123-
defaultControlPlaneCPU = 6
124-
defaultControlPlaneMemory = 16
123+
defaultControlPlaneCPU = 6
124+
defaultControlPlaneMemory = 16
125+
defaultControlPlaneNodeCount = 3
126+
defaultControlPlaneSchedulable = false
125127
)
126128

129+
// Check if hosted control plane is enabled
130+
hostedControlPlane := false
131+
if apiReq.HostedControlPlane != nil {
132+
hostedControlPlane = *apiReq.HostedControlPlane
133+
}
134+
127135
form := mappers.ClusterRequirementsRequestForm{
128-
ClusterID: apiReq.ClusterId,
129-
CpuOverCommitRatio: string(apiReq.CpuOverCommitRatio),
130-
MemoryOverCommitRatio: string(apiReq.MemoryOverCommitRatio),
131-
WorkerNodeCPU: apiReq.WorkerNodeCPU,
132-
WorkerNodeThreads: 0,
133-
WorkerNodeMemory: apiReq.WorkerNodeMemory,
134-
ControlPlaneSchedulable: false,
135-
ControlPlaneCPU: defaultControlPlaneCPU,
136-
ControlPlaneMemory: defaultControlPlaneMemory,
137-
}
138-
if apiReq.ControlPlaneSchedulable != nil {
139-
form.ControlPlaneSchedulable = *apiReq.ControlPlaneSchedulable
136+
ClusterID: apiReq.ClusterId,
137+
CpuOverCommitRatio: string(apiReq.CpuOverCommitRatio),
138+
MemoryOverCommitRatio: string(apiReq.MemoryOverCommitRatio),
139+
WorkerNodeCPU: apiReq.WorkerNodeCPU,
140+
WorkerNodeThreads: 0,
141+
WorkerNodeMemory: apiReq.WorkerNodeMemory,
142+
HostedControlPlane: hostedControlPlane,
140143
}
144+
141145
if apiReq.WorkerNodeThreads != nil {
142146
form.WorkerNodeThreads = *apiReq.WorkerNodeThreads
143147
}
144-
if apiReq.ControlPlaneCPU != nil {
145-
form.ControlPlaneCPU = *apiReq.ControlPlaneCPU
146-
}
147-
if apiReq.ControlPlaneMemory != nil {
148-
form.ControlPlaneMemory = *apiReq.ControlPlaneMemory
149-
}
150-
if apiReq.ControlPlaneNodeCount != nil {
151-
form.ControlPlaneNodeCount = int(*apiReq.ControlPlaneNodeCount)
152-
} else {
153-
form.ControlPlaneNodeCount = int(v1alpha1.N3)
148+
149+
// Only set control plane fields when NOT using hosted control plane
150+
if !hostedControlPlane {
151+
// Apply defaults or use provided values for control plane fields
152+
if apiReq.ControlPlaneSchedulable != nil {
153+
form.ControlPlaneSchedulable = *apiReq.ControlPlaneSchedulable
154+
} else {
155+
form.ControlPlaneSchedulable = defaultControlPlaneSchedulable
156+
}
157+
158+
if apiReq.ControlPlaneCPU != nil {
159+
form.ControlPlaneCPU = *apiReq.ControlPlaneCPU
160+
} else {
161+
form.ControlPlaneCPU = defaultControlPlaneCPU
162+
}
163+
164+
if apiReq.ControlPlaneMemory != nil {
165+
form.ControlPlaneMemory = *apiReq.ControlPlaneMemory
166+
} else {
167+
form.ControlPlaneMemory = defaultControlPlaneMemory
168+
}
169+
170+
if apiReq.ControlPlaneNodeCount != nil {
171+
form.ControlPlaneNodeCount = int(*apiReq.ControlPlaneNodeCount)
172+
} else {
173+
form.ControlPlaneNodeCount = defaultControlPlaneNodeCount
174+
}
154175
}
176+
// When hostedControlPlane is true, control plane fields remain at zero values
177+
155178
return form
156179
}
157180

internal/handlers/v1alpha1/sizer.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
api "github.com/kubev2v/migration-planner/api/v1alpha1"
78
"github.com/kubev2v/migration-planner/internal/api/server"
89
"github.com/kubev2v/migration-planner/internal/auth"
910
"github.com/kubev2v/migration-planner/internal/handlers/v1alpha1/mappers"
@@ -55,14 +56,22 @@ func (h *ServiceHandler) CalculateAssessmentClusterRequirements(ctx context.Cont
5556
return server.CalculateAssessmentClusterRequirements400JSONResponse{Message: fmt.Sprintf("invalid memory over-commit ratio: %s. Valid values are: 1:1, 1:2, 1:4", request.Body.MemoryOverCommitRatio)}, nil
5657
}
5758

59+
// Validate that control plane fields are not provided when hosted control plane is enabled
60+
if err := validateNoControlPlaneFieldsWhenHosted(request.Body); err != nil {
61+
logger.Error(err).Log()
62+
return server.CalculateAssessmentClusterRequirements400JSONResponse{Message: err.Error()}, nil
63+
}
64+
5865
// 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
66+
if request.Body.HostedControlPlane == nil || !*request.Body.HostedControlPlane {
67+
if request.Body.ControlPlaneNodeCount != nil {
68+
count := int(*request.Body.ControlPlaneNodeCount)
69+
if count != 1 && count != 3 {
70+
logger.Error(fmt.Errorf("invalid controlPlaneNodeCount: %d", count)).Log()
71+
return server.CalculateAssessmentClusterRequirements400JSONResponse{
72+
Message: fmt.Sprintf("invalid controlPlaneNodeCount: %d", count),
73+
}, nil
74+
}
6675
}
6776
}
6877

@@ -135,3 +144,27 @@ func (h *ServiceHandler) CalculateAssessmentClusterRequirements(ctx context.Cont
135144
apiResponse := mappers.ClusterRequirementsResponseFormToAPI(*res)
136145
return server.CalculateAssessmentClusterRequirements200JSONResponse(apiResponse), nil
137146
}
147+
148+
// validateNoControlPlaneFieldsWhenHosted checks that control plane fields are not provided
149+
// when hosted control plane mode is enabled. Returns an error if any control plane field is present.
150+
func validateNoControlPlaneFieldsWhenHosted(req *api.ClusterRequirementsRequest) error {
151+
if req.HostedControlPlane == nil || !*req.HostedControlPlane {
152+
return nil
153+
}
154+
155+
// Check each control plane field directly (not through interface{} to avoid nil pointer issues)
156+
if req.ControlPlaneNodeCount != nil {
157+
return fmt.Errorf("controlPlaneNodeCount cannot be specified when hostedControlPlane is true")
158+
}
159+
if req.ControlPlaneCPU != nil {
160+
return fmt.Errorf("controlPlaneCPU cannot be specified when hostedControlPlane is true")
161+
}
162+
if req.ControlPlaneMemory != nil {
163+
return fmt.Errorf("controlPlaneMemory cannot be specified when hostedControlPlane is true")
164+
}
165+
if req.ControlPlaneSchedulable != nil {
166+
return fmt.Errorf("controlPlaneSchedulable cannot be specified when hostedControlPlane is true")
167+
}
168+
169+
return nil
170+
}

internal/handlers/v1alpha1/sizer_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,120 @@ 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+
request := &api.ClusterRequirementsRequest{
360+
ClusterId: clusterID,
361+
CpuOverCommitRatio: api.CpuOneToFour,
362+
MemoryOverCommitRatio: api.MemoryOneToTwo,
363+
WorkerNodeCPU: 8,
364+
WorkerNodeMemory: 16,
365+
HostedControlPlane: &hostedTrue,
366+
// Control plane fields must not be specified when hostedControlPlane is true
367+
}
368+
369+
mockStore.assessments[assessmentID] = createTestAssessment(assessmentID, user.Username, user.Organization, clusterID)
370+
testServer = createTestSizerServer(createTestSizerResponse(4, 4, 0, 40, 80), http.StatusOK, false)
371+
sizerClient = client.NewSizerClient(testServer.URL, 5*time.Second)
372+
handler = handlers.NewServiceHandler(
373+
nil,
374+
service.NewAssessmentService(mockStore, nil),
375+
nil,
376+
service.NewSizerService(sizerClient, mockStore),
377+
nil,
378+
)
379+
380+
resp, err := handler.CalculateAssessmentClusterRequirements(ctx, server.CalculateAssessmentClusterRequirementsRequestObject{
381+
Id: assessmentID,
382+
Body: request,
383+
})
384+
385+
Expect(err).To(BeNil())
386+
Expect(resp).NotTo(BeNil())
387+
successResp, ok := resp.(server.CalculateAssessmentClusterRequirements200JSONResponse)
388+
Expect(ok).To(BeTrue())
389+
Expect(successResp.ClusterSizing.ControlPlaneNodes).To(Equal(0))
390+
Expect(successResp.ClusterSizing.WorkerNodes).To(Equal(6))
391+
Expect(successResp.ClusterSizing.TotalNodes).To(Equal(6))
392+
})
393+
394+
type hostedControlPlaneValidationCase struct {
395+
name string
396+
setupRequest func(*api.ClusterRequirementsRequest)
397+
expectedError string
398+
}
399+
400+
hostedControlPlaneValidationCases := []hostedControlPlaneValidationCase{
401+
{
402+
name: "returns 400 when hostedControlPlane is true and controlPlaneNodeCount is specified",
403+
setupRequest: func(req *api.ClusterRequirementsRequest) {
404+
count := api.ClusterRequirementsRequestControlPlaneNodeCount(3)
405+
req.ControlPlaneNodeCount = &count
406+
},
407+
expectedError: "controlPlaneNodeCount cannot be specified when hostedControlPlane is true",
408+
},
409+
{
410+
name: "returns 400 when hostedControlPlane is true and controlPlaneCPU is specified",
411+
setupRequest: func(req *api.ClusterRequirementsRequest) {
412+
cpu := 8
413+
req.ControlPlaneCPU = &cpu
414+
},
415+
expectedError: "controlPlaneCPU cannot be specified when hostedControlPlane is true",
416+
},
417+
{
418+
name: "returns 400 when hostedControlPlane is true and controlPlaneMemory is specified",
419+
setupRequest: func(req *api.ClusterRequirementsRequest) {
420+
memory := 16
421+
req.ControlPlaneMemory = &memory
422+
},
423+
expectedError: "controlPlaneMemory cannot be specified when hostedControlPlane is true",
424+
},
425+
{
426+
name: "returns 400 when hostedControlPlane is true and controlPlaneSchedulable is specified",
427+
setupRequest: func(req *api.ClusterRequirementsRequest) {
428+
schedulable := false
429+
req.ControlPlaneSchedulable = &schedulable
430+
},
431+
expectedError: "controlPlaneSchedulable cannot be specified when hostedControlPlane is true",
432+
},
433+
}
434+
435+
for _, tc := range hostedControlPlaneValidationCases {
436+
tc := tc
437+
It(tc.name, func() {
438+
hostedTrue := true
439+
request := &api.ClusterRequirementsRequest{
440+
ClusterId: clusterID,
441+
CpuOverCommitRatio: api.CpuOneToFour,
442+
MemoryOverCommitRatio: api.MemoryOneToTwo,
443+
WorkerNodeCPU: 8,
444+
WorkerNodeMemory: 16,
445+
HostedControlPlane: &hostedTrue,
446+
}
447+
tc.setupRequest(request)
448+
449+
mockStore.assessments[assessmentID] = createTestAssessment(assessmentID, user.Username, user.Organization, clusterID)
450+
handler = handlers.NewServiceHandler(
451+
nil,
452+
service.NewAssessmentService(mockStore, nil),
453+
nil,
454+
nil,
455+
nil,
456+
)
457+
458+
resp, err := handler.CalculateAssessmentClusterRequirements(ctx, server.CalculateAssessmentClusterRequirementsRequestObject{
459+
Id: assessmentID,
460+
Body: request,
461+
})
462+
463+
Expect(err).To(BeNil())
464+
Expect(resp).NotTo(BeNil())
465+
errorResp, ok := resp.(server.CalculateAssessmentClusterRequirements400JSONResponse)
466+
Expect(ok).To(BeTrue())
467+
Expect(errorResp.Message).To(Equal(tc.expectedError))
468+
})
469+
}
470+
357471
intPtr := func(v int) *int { return &v }
358472
cases := []struct {
359473
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 {

0 commit comments

Comments
 (0)