Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions api/v1alpha1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1752,27 +1752,26 @@ components:
validate: "omitempty,min=2"
controlPlaneSchedulable:
type: boolean
default: false
description: Allow workload scheduling on control plane nodes
description: "Allow workload scheduling on control plane nodes (default: false)"
controlPlaneCPU:
type: integer
default: 6
description: "CPU cores per control plane node (default: 6)"
x-oapi-codegen-extra-tags:
validate: "omitempty,min=2,max=200"
controlPlaneMemory:
type: integer
default: 16
description: "Memory (GB) per control plane node (default: 16)"
description: "Memory in GB per control plane node (default: 16)"
x-oapi-codegen-extra-tags:
validate: "omitempty,min=4,max=512"
controlPlaneNodeCount:
type: integer
enum: [1, 3]
default: 3
description: Number of control plane nodes (1 for single node, 3 for HA)
description: "Number of control plane nodes: 1 or 3 (default: 3)"
x-oapi-codegen-extra-tags:
validate: "omitempty,oneof=1 3"
hostedControlPlane:
type: boolean
description: "If true, control plane is hosted externally. Incompatible with control plane fields (default: false)"
required:
- clusterId
- cpuOverCommitRatio
Expand Down
196 changes: 98 additions & 98 deletions api/v1alpha1/spec.gen.go

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions api/v1alpha1/types.gen.go

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

71 changes: 47 additions & 24 deletions internal/handlers/v1alpha1/mappers/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,38 +120,61 @@ func ClusterRequirementsRequestToForm(apiReq v1alpha1.ClusterRequirementsRequest
// Default control plane node resources align with OVF sizer defaults and meet
// Assisted Installer prerequisites for control plane nodes.
const (
defaultControlPlaneCPU = 6
defaultControlPlaneMemory = 16
defaultControlPlaneCPU = 6
defaultControlPlaneMemory = 16
defaultControlPlaneNodeCount = 3
defaultControlPlaneSchedulable = false
)

// Check if hosted control plane is enabled
hostedControlPlane := false
if apiReq.HostedControlPlane != nil {
hostedControlPlane = *apiReq.HostedControlPlane
}

form := mappers.ClusterRequirementsRequestForm{
ClusterID: apiReq.ClusterId,
CpuOverCommitRatio: string(apiReq.CpuOverCommitRatio),
MemoryOverCommitRatio: string(apiReq.MemoryOverCommitRatio),
WorkerNodeCPU: apiReq.WorkerNodeCPU,
WorkerNodeThreads: 0,
WorkerNodeMemory: apiReq.WorkerNodeMemory,
ControlPlaneSchedulable: false,
ControlPlaneCPU: defaultControlPlaneCPU,
ControlPlaneMemory: defaultControlPlaneMemory,
}
if apiReq.ControlPlaneSchedulable != nil {
form.ControlPlaneSchedulable = *apiReq.ControlPlaneSchedulable
ClusterID: apiReq.ClusterId,
CpuOverCommitRatio: string(apiReq.CpuOverCommitRatio),
MemoryOverCommitRatio: string(apiReq.MemoryOverCommitRatio),
WorkerNodeCPU: apiReq.WorkerNodeCPU,
WorkerNodeThreads: 0,
WorkerNodeMemory: apiReq.WorkerNodeMemory,
HostedControlPlane: hostedControlPlane,
}

if apiReq.WorkerNodeThreads != nil {
form.WorkerNodeThreads = *apiReq.WorkerNodeThreads
}
if apiReq.ControlPlaneCPU != nil {
form.ControlPlaneCPU = *apiReq.ControlPlaneCPU
}
if apiReq.ControlPlaneMemory != nil {
form.ControlPlaneMemory = *apiReq.ControlPlaneMemory
}
if apiReq.ControlPlaneNodeCount != nil {
form.ControlPlaneNodeCount = int(*apiReq.ControlPlaneNodeCount)
} else {
form.ControlPlaneNodeCount = int(v1alpha1.N3)

// Only set control plane fields when NOT using hosted control plane
if !hostedControlPlane {
// Apply defaults or use provided values for control plane fields
if apiReq.ControlPlaneSchedulable != nil {
form.ControlPlaneSchedulable = *apiReq.ControlPlaneSchedulable
} else {
form.ControlPlaneSchedulable = defaultControlPlaneSchedulable
}

if apiReq.ControlPlaneCPU != nil {
form.ControlPlaneCPU = *apiReq.ControlPlaneCPU
} else {
form.ControlPlaneCPU = defaultControlPlaneCPU
}

if apiReq.ControlPlaneMemory != nil {
form.ControlPlaneMemory = *apiReq.ControlPlaneMemory
} else {
form.ControlPlaneMemory = defaultControlPlaneMemory
}

if apiReq.ControlPlaneNodeCount != nil {
form.ControlPlaneNodeCount = int(*apiReq.ControlPlaneNodeCount)
} else {
form.ControlPlaneNodeCount = defaultControlPlaneNodeCount
}
}
// When hostedControlPlane is true, control plane fields remain at zero values

return form
}

Expand Down
47 changes: 40 additions & 7 deletions internal/handlers/v1alpha1/sizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

api "github.com/kubev2v/migration-planner/api/v1alpha1"
"github.com/kubev2v/migration-planner/internal/api/server"
"github.com/kubev2v/migration-planner/internal/auth"
"github.com/kubev2v/migration-planner/internal/handlers/v1alpha1/mappers"
Expand Down Expand Up @@ -55,14 +56,22 @@ func (h *ServiceHandler) CalculateAssessmentClusterRequirements(ctx context.Cont
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
}

// Validate that control plane fields are not provided when hosted control plane is enabled
if err := validateNoControlPlaneFieldsWhenHosted(request.Body); err != nil {
logger.Error(err).Log()
return server.CalculateAssessmentClusterRequirements400JSONResponse{Message: err.Error()}, nil
}

// Validate controlPlaneNodeCount (API enum handles this in production, but tests bypass middleware)
if request.Body.ControlPlaneNodeCount != nil {
count := int(*request.Body.ControlPlaneNodeCount)
if count != 1 && count != 3 {
logger.Error(fmt.Errorf("invalid controlPlaneNodeCount: %d", count)).Log()
return server.CalculateAssessmentClusterRequirements400JSONResponse{
Message: fmt.Sprintf("invalid controlPlaneNodeCount: %d", count),
}, nil
if request.Body.HostedControlPlane == nil || !*request.Body.HostedControlPlane {
if request.Body.ControlPlaneNodeCount != nil {
count := int(*request.Body.ControlPlaneNodeCount)
if count != 1 && count != 3 {
logger.Error(fmt.Errorf("invalid controlPlaneNodeCount: %d", count)).Log()
return server.CalculateAssessmentClusterRequirements400JSONResponse{
Message: fmt.Sprintf("invalid controlPlaneNodeCount: %d", count),
}, nil
}
}
}

Expand Down Expand Up @@ -135,3 +144,27 @@ func (h *ServiceHandler) CalculateAssessmentClusterRequirements(ctx context.Cont
apiResponse := mappers.ClusterRequirementsResponseFormToAPI(*res)
return server.CalculateAssessmentClusterRequirements200JSONResponse(apiResponse), nil
}

// validateNoControlPlaneFieldsWhenHosted checks that control plane fields are not provided
// when hosted control plane mode is enabled. Returns an error if any control plane field is present.
func validateNoControlPlaneFieldsWhenHosted(req *api.ClusterRequirementsRequest) error {
if req.HostedControlPlane == nil || !*req.HostedControlPlane {
return nil
}

// Check each control plane field directly (not through interface{} to avoid nil pointer issues)
if req.ControlPlaneNodeCount != nil {
return fmt.Errorf("controlPlaneNodeCount cannot be specified when hostedControlPlane is true")
}
if req.ControlPlaneCPU != nil {
return fmt.Errorf("controlPlaneCPU cannot be specified when hostedControlPlane is true")
}
if req.ControlPlaneMemory != nil {
return fmt.Errorf("controlPlaneMemory cannot be specified when hostedControlPlane is true")
}
if req.ControlPlaneSchedulable != nil {
return fmt.Errorf("controlPlaneSchedulable cannot be specified when hostedControlPlane is true")
}

return nil
}
114 changes: 114 additions & 0 deletions internal/handlers/v1alpha1/sizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,120 @@ var _ = Describe("sizer handler", func() {
Expect(successResp.ResourceConsumption.Memory).To(Equal(200.0))
})

It("returns 200 with hostedControlPlane true and zero control plane nodes in response", func() {
hostedTrue := true
request := &api.ClusterRequirementsRequest{
ClusterId: clusterID,
CpuOverCommitRatio: api.CpuOneToFour,
MemoryOverCommitRatio: api.MemoryOneToTwo,
WorkerNodeCPU: 8,
WorkerNodeMemory: 16,
HostedControlPlane: &hostedTrue,
// Control plane fields must not be specified when hostedControlPlane is true
}

mockStore.assessments[assessmentID] = createTestAssessment(assessmentID, user.Username, user.Organization, clusterID)
testServer = createTestSizerServer(createTestSizerResponse(4, 4, 0, 40, 80), http.StatusOK, false)
sizerClient = client.NewSizerClient(testServer.URL, 5*time.Second)
handler = handlers.NewServiceHandler(
nil,
service.NewAssessmentService(mockStore, nil),
nil,
service.NewSizerService(sizerClient, mockStore),
nil,
)

resp, err := handler.CalculateAssessmentClusterRequirements(ctx, server.CalculateAssessmentClusterRequirementsRequestObject{
Id: assessmentID,
Body: request,
})

Expect(err).To(BeNil())
Expect(resp).NotTo(BeNil())
successResp, ok := resp.(server.CalculateAssessmentClusterRequirements200JSONResponse)
Expect(ok).To(BeTrue())
Expect(successResp.ClusterSizing.ControlPlaneNodes).To(Equal(0))
Expect(successResp.ClusterSizing.WorkerNodes).To(Equal(6))
Expect(successResp.ClusterSizing.TotalNodes).To(Equal(6))
})

type hostedControlPlaneValidationCase struct {
name string
setupRequest func(*api.ClusterRequirementsRequest)
expectedError string
}

hostedControlPlaneValidationCases := []hostedControlPlaneValidationCase{
{
name: "returns 400 when hostedControlPlane is true and controlPlaneNodeCount is specified",
setupRequest: func(req *api.ClusterRequirementsRequest) {
count := api.ClusterRequirementsRequestControlPlaneNodeCount(3)
req.ControlPlaneNodeCount = &count
},
expectedError: "controlPlaneNodeCount cannot be specified when hostedControlPlane is true",
},
{
name: "returns 400 when hostedControlPlane is true and controlPlaneCPU is specified",
setupRequest: func(req *api.ClusterRequirementsRequest) {
cpu := 8
req.ControlPlaneCPU = &cpu
},
expectedError: "controlPlaneCPU cannot be specified when hostedControlPlane is true",
},
{
name: "returns 400 when hostedControlPlane is true and controlPlaneMemory is specified",
setupRequest: func(req *api.ClusterRequirementsRequest) {
memory := 16
req.ControlPlaneMemory = &memory
},
expectedError: "controlPlaneMemory cannot be specified when hostedControlPlane is true",
},
{
name: "returns 400 when hostedControlPlane is true and controlPlaneSchedulable is specified",
setupRequest: func(req *api.ClusterRequirementsRequest) {
schedulable := false
req.ControlPlaneSchedulable = &schedulable
},
expectedError: "controlPlaneSchedulable cannot be specified when hostedControlPlane is true",
},
}

for _, tc := range hostedControlPlaneValidationCases {
tc := tc
It(tc.name, func() {
hostedTrue := true
request := &api.ClusterRequirementsRequest{
ClusterId: clusterID,
CpuOverCommitRatio: api.CpuOneToFour,
MemoryOverCommitRatio: api.MemoryOneToTwo,
WorkerNodeCPU: 8,
WorkerNodeMemory: 16,
HostedControlPlane: &hostedTrue,
}
tc.setupRequest(request)

mockStore.assessments[assessmentID] = createTestAssessment(assessmentID, user.Username, user.Organization, clusterID)
handler = handlers.NewServiceHandler(
nil,
service.NewAssessmentService(mockStore, nil),
nil,
nil,
nil,
)

resp, err := handler.CalculateAssessmentClusterRequirements(ctx, server.CalculateAssessmentClusterRequirementsRequestObject{
Id: assessmentID,
Body: request,
})

Expect(err).To(BeNil())
Expect(resp).NotTo(BeNil())
errorResp, ok := resp.(server.CalculateAssessmentClusterRequirements400JSONResponse)
Expect(ok).To(BeTrue())
Expect(errorResp.Message).To(Equal(tc.expectedError))
})
}

intPtr := func(v int) *int { return &v }
cases := []struct {
name string
Expand Down
1 change: 1 addition & 0 deletions internal/service/mappers/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ type ClusterRequirementsRequestForm struct {
ControlPlaneCPU int
ControlPlaneMemory int
ControlPlaneNodeCount int
HostedControlPlane bool
}

type ClusterRequirementsResponseForm struct {
Expand Down
Loading
Loading