From 21158e6c7adb35b7073da33a2608c47934c4a38e Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Fri, 17 Dec 2021 12:29:10 -0600 Subject: [PATCH] Added CPU size selector & add tenant cleanup (#1326) --- models/allocatable_resources_response.go | 165 +++++++++ models/node_max_allocatable_resources.go | 70 ++++ operatorapi/embedded_spec.go | 130 +++++++ operatorapi/operations/operator_api.go | 12 + .../operator_api/get_allocatable_resources.go | 88 +++++ .../get_allocatable_resources_parameters.go | 120 +++++++ .../get_allocatable_resources_responses.go | 133 +++++++ .../get_allocatable_resources_urlbuilder.go | 119 +++++++ operatorapi/operator_nodes.go | 159 +++++++++ portal-ui/src/common/types.ts | 2 + portal-ui/src/common/utils.ts | 112 ++++++ .../InputBoxWrapper/InputBoxWrapper.tsx | 6 + .../Console/Tenants/AddTenant/AddTenant.tsx | 8 +- .../Tenants/AddTenant/Steps/SizePreview.tsx | 221 +----------- .../Tenants/AddTenant/Steps/TenantSize.tsx | 325 +++++++++++------- .../Console/Tenants/ListTenants/types.ts | 8 +- .../src/screens/Console/Tenants/reducer.ts | 58 +++- .../src/screens/Console/Tenants/types.ts | 20 +- swagger-operator.yml | 103 ++++-- 19 files changed, 1489 insertions(+), 370 deletions(-) create mode 100644 models/allocatable_resources_response.go create mode 100644 models/node_max_allocatable_resources.go create mode 100644 operatorapi/operations/operator_api/get_allocatable_resources.go create mode 100644 operatorapi/operations/operator_api/get_allocatable_resources_parameters.go create mode 100644 operatorapi/operations/operator_api/get_allocatable_resources_responses.go create mode 100644 operatorapi/operations/operator_api/get_allocatable_resources_urlbuilder.go diff --git a/models/allocatable_resources_response.go b/models/allocatable_resources_response.go new file mode 100644 index 000000000..a0600dcd0 --- /dev/null +++ b/models/allocatable_resources_response.go @@ -0,0 +1,165 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// AllocatableResourcesResponse allocatable resources response +// +// swagger:model allocatableResourcesResponse +type AllocatableResourcesResponse struct { + + // cpu priority + CPUPriority *NodeMaxAllocatableResources `json:"cpu_priority,omitempty"` + + // mem priority + MemPriority *NodeMaxAllocatableResources `json:"mem_priority,omitempty"` + + // min allocatable cpu + MinAllocatableCPU int64 `json:"min_allocatable_cpu,omitempty"` + + // min allocatable mem + MinAllocatableMem int64 `json:"min_allocatable_mem,omitempty"` +} + +// Validate validates this allocatable resources response +func (m *AllocatableResourcesResponse) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCPUPriority(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMemPriority(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AllocatableResourcesResponse) validateCPUPriority(formats strfmt.Registry) error { + if swag.IsZero(m.CPUPriority) { // not required + return nil + } + + if m.CPUPriority != nil { + if err := m.CPUPriority.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("cpu_priority") + } + return err + } + } + + return nil +} + +func (m *AllocatableResourcesResponse) validateMemPriority(formats strfmt.Registry) error { + if swag.IsZero(m.MemPriority) { // not required + return nil + } + + if m.MemPriority != nil { + if err := m.MemPriority.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("mem_priority") + } + return err + } + } + + return nil +} + +// ContextValidate validate this allocatable resources response based on the context it is used +func (m *AllocatableResourcesResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCPUPriority(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateMemPriority(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *AllocatableResourcesResponse) contextValidateCPUPriority(ctx context.Context, formats strfmt.Registry) error { + + if m.CPUPriority != nil { + if err := m.CPUPriority.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("cpu_priority") + } + return err + } + } + + return nil +} + +func (m *AllocatableResourcesResponse) contextValidateMemPriority(ctx context.Context, formats strfmt.Registry) error { + + if m.MemPriority != nil { + if err := m.MemPriority.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("mem_priority") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *AllocatableResourcesResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *AllocatableResourcesResponse) UnmarshalBinary(b []byte) error { + var res AllocatableResourcesResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/node_max_allocatable_resources.go b/models/node_max_allocatable_resources.go new file mode 100644 index 000000000..1a5e8525c --- /dev/null +++ b/models/node_max_allocatable_resources.go @@ -0,0 +1,70 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NodeMaxAllocatableResources node max allocatable resources +// +// swagger:model nodeMaxAllocatableResources +type NodeMaxAllocatableResources struct { + + // max allocatable cpu + MaxAllocatableCPU int64 `json:"max_allocatable_cpu,omitempty"` + + // max allocatable mem + MaxAllocatableMem int64 `json:"max_allocatable_mem,omitempty"` +} + +// Validate validates this node max allocatable resources +func (m *NodeMaxAllocatableResources) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this node max allocatable resources based on context it is used +func (m *NodeMaxAllocatableResources) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *NodeMaxAllocatableResources) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *NodeMaxAllocatableResources) UnmarshalBinary(b []byte) error { + var res NodeMaxAllocatableResources + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/operatorapi/embedded_spec.go b/operatorapi/embedded_spec.go index b52bca1af..ebc6c991a 100644 --- a/operatorapi/embedded_spec.go +++ b/operatorapi/embedded_spec.go @@ -52,6 +52,39 @@ func init() { }, "basePath": "/api/v1", "paths": { + "/cluster/allocatable-resources": { + "get": { + "tags": [ + "OperatorAPI" + ], + "summary": "Get allocatable cpu and memory for given number of nodes", + "operationId": "GetAllocatableResources", + "parameters": [ + { + "minimum": 1, + "type": "integer", + "format": "int32", + "name": "num_nodes", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/allocatableResourcesResponse" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, "/cluster/max-allocatable-memory": { "get": { "tags": [ @@ -1382,6 +1415,25 @@ func init() { } }, "definitions": { + "allocatableResourcesResponse": { + "type": "object", + "properties": { + "cpu_priority": { + "$ref": "#/definitions/nodeMaxAllocatableResources" + }, + "mem_priority": { + "$ref": "#/definitions/nodeMaxAllocatableResources" + }, + "min_allocatable_cpu": { + "type": "integer", + "format": "int64" + }, + "min_allocatable_mem": { + "type": "integer", + "format": "int64" + } + } + }, "awsConfiguration": { "type": "object", "required": [ @@ -2229,6 +2281,19 @@ func init() { } } }, + "nodeMaxAllocatableResources": { + "type": "object", + "properties": { + "max_allocatable_cpu": { + "type": "integer", + "format": "int64" + }, + "max_allocatable_mem": { + "type": "integer", + "format": "int64" + } + } + }, "nodeSelectorTerm": { "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", "type": "object", @@ -3187,6 +3252,39 @@ func init() { }, "basePath": "/api/v1", "paths": { + "/cluster/allocatable-resources": { + "get": { + "tags": [ + "OperatorAPI" + ], + "summary": "Get allocatable cpu and memory for given number of nodes", + "operationId": "GetAllocatableResources", + "parameters": [ + { + "minimum": 1, + "type": "integer", + "format": "int32", + "name": "num_nodes", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/allocatableResourcesResponse" + } + }, + "default": { + "description": "Generic error response.", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, "/cluster/max-allocatable-memory": { "get": { "tags": [ @@ -5242,6 +5340,25 @@ func init() { } } }, + "allocatableResourcesResponse": { + "type": "object", + "properties": { + "cpu_priority": { + "$ref": "#/definitions/nodeMaxAllocatableResources" + }, + "mem_priority": { + "$ref": "#/definitions/nodeMaxAllocatableResources" + }, + "min_allocatable_cpu": { + "type": "integer", + "format": "int64" + }, + "min_allocatable_mem": { + "type": "integer", + "format": "int64" + } + } + }, "awsConfiguration": { "type": "object", "required": [ @@ -6077,6 +6194,19 @@ func init() { } } }, + "nodeMaxAllocatableResources": { + "type": "object", + "properties": { + "max_allocatable_cpu": { + "type": "integer", + "format": "int64" + }, + "max_allocatable_mem": { + "type": "integer", + "format": "int64" + } + } + }, "nodeSelectorTerm": { "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", "type": "object", diff --git a/operatorapi/operations/operator_api.go b/operatorapi/operations/operator_api.go index 00c61245b..b2f5dde07 100644 --- a/operatorapi/operations/operator_api.go +++ b/operatorapi/operations/operator_api.go @@ -78,6 +78,9 @@ func NewOperatorAPI(spec *loads.Document) *OperatorAPI { OperatorAPIDirectCSIFormatDriveHandler: operator_api.DirectCSIFormatDriveHandlerFunc(func(params operator_api.DirectCSIFormatDriveParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation operator_api.DirectCSIFormatDrive has not yet been implemented") }), + OperatorAPIGetAllocatableResourcesHandler: operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation operator_api.GetAllocatableResources has not yet been implemented") + }), OperatorAPIGetDirectCSIDriveListHandler: operator_api.GetDirectCSIDriveListHandlerFunc(func(params operator_api.GetDirectCSIDriveListParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation operator_api.GetDirectCSIDriveList has not yet been implemented") }), @@ -236,6 +239,8 @@ type OperatorAPI struct { OperatorAPIDeleteTenantHandler operator_api.DeleteTenantHandler // OperatorAPIDirectCSIFormatDriveHandler sets the operation handler for the direct c s i format drive operation OperatorAPIDirectCSIFormatDriveHandler operator_api.DirectCSIFormatDriveHandler + // OperatorAPIGetAllocatableResourcesHandler sets the operation handler for the get allocatable resources operation + OperatorAPIGetAllocatableResourcesHandler operator_api.GetAllocatableResourcesHandler // OperatorAPIGetDirectCSIDriveListHandler sets the operation handler for the get direct c s i drive list operation OperatorAPIGetDirectCSIDriveListHandler operator_api.GetDirectCSIDriveListHandler // OperatorAPIGetDirectCSIVolumeListHandler sets the operation handler for the get direct c s i volume list operation @@ -398,6 +403,9 @@ func (o *OperatorAPI) Validate() error { if o.OperatorAPIDirectCSIFormatDriveHandler == nil { unregistered = append(unregistered, "operator_api.DirectCSIFormatDriveHandler") } + if o.OperatorAPIGetAllocatableResourcesHandler == nil { + unregistered = append(unregistered, "operator_api.GetAllocatableResourcesHandler") + } if o.OperatorAPIGetDirectCSIDriveListHandler == nil { unregistered = append(unregistered, "operator_api.GetDirectCSIDriveListHandler") } @@ -618,6 +626,10 @@ func (o *OperatorAPI) initHandlerCache() { if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } + o.handlers["GET"]["/cluster/allocatable-resources"] = operator_api.NewGetAllocatableResources(o.context, o.OperatorAPIGetAllocatableResourcesHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } o.handlers["GET"]["/direct-csi/drives"] = operator_api.NewGetDirectCSIDriveList(o.context, o.OperatorAPIGetDirectCSIDriveListHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) diff --git a/operatorapi/operations/operator_api/get_allocatable_resources.go b/operatorapi/operations/operator_api/get_allocatable_resources.go new file mode 100644 index 000000000..40823f157 --- /dev/null +++ b/operatorapi/operations/operator_api/get_allocatable_resources.go @@ -0,0 +1,88 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package operator_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/minio/console/models" +) + +// GetAllocatableResourcesHandlerFunc turns a function with the right signature into a get allocatable resources handler +type GetAllocatableResourcesHandlerFunc func(GetAllocatableResourcesParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetAllocatableResourcesHandlerFunc) Handle(params GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// GetAllocatableResourcesHandler interface for that can handle valid get allocatable resources params +type GetAllocatableResourcesHandler interface { + Handle(GetAllocatableResourcesParams, *models.Principal) middleware.Responder +} + +// NewGetAllocatableResources creates a new http.Handler for the get allocatable resources operation +func NewGetAllocatableResources(ctx *middleware.Context, handler GetAllocatableResourcesHandler) *GetAllocatableResources { + return &GetAllocatableResources{Context: ctx, Handler: handler} +} + +/* GetAllocatableResources swagger:route GET /cluster/allocatable-resources OperatorAPI getAllocatableResources + +Get allocatable cpu and memory for given number of nodes + +*/ +type GetAllocatableResources struct { + Context *middleware.Context + Handler GetAllocatableResourcesHandler +} + +func (o *GetAllocatableResources) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetAllocatableResourcesParams() + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + *r = *aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/operatorapi/operations/operator_api/get_allocatable_resources_parameters.go b/operatorapi/operations/operator_api/get_allocatable_resources_parameters.go new file mode 100644 index 000000000..b49bfa928 --- /dev/null +++ b/operatorapi/operations/operator_api/get_allocatable_resources_parameters.go @@ -0,0 +1,120 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package operator_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// NewGetAllocatableResourcesParams creates a new GetAllocatableResourcesParams object +// +// There are no default values defined in the spec. +func NewGetAllocatableResourcesParams() GetAllocatableResourcesParams { + + return GetAllocatableResourcesParams{} +} + +// GetAllocatableResourcesParams contains all the bound params for the get allocatable resources operation +// typically these are obtained from a http.Request +// +// swagger:parameters GetAllocatableResources +type GetAllocatableResourcesParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + Minimum: 1 + In: query + */ + NumNodes int32 +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetAllocatableResourcesParams() beforehand. +func (o *GetAllocatableResourcesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + qNumNodes, qhkNumNodes, _ := qs.GetOK("num_nodes") + if err := o.bindNumNodes(qNumNodes, qhkNumNodes, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindNumNodes binds and validates parameter NumNodes from query. +func (o *GetAllocatableResourcesParams) bindNumNodes(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("num_nodes", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + + if err := validate.RequiredString("num_nodes", "query", raw); err != nil { + return err + } + + value, err := swag.ConvertInt32(raw) + if err != nil { + return errors.InvalidType("num_nodes", "query", "int32", raw) + } + o.NumNodes = value + + if err := o.validateNumNodes(formats); err != nil { + return err + } + + return nil +} + +// validateNumNodes carries on validations for parameter NumNodes +func (o *GetAllocatableResourcesParams) validateNumNodes(formats strfmt.Registry) error { + + if err := validate.MinimumInt("num_nodes", "query", int64(o.NumNodes), 1, false); err != nil { + return err + } + + return nil +} diff --git a/operatorapi/operations/operator_api/get_allocatable_resources_responses.go b/operatorapi/operations/operator_api/get_allocatable_resources_responses.go new file mode 100644 index 000000000..227d319ac --- /dev/null +++ b/operatorapi/operations/operator_api/get_allocatable_resources_responses.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package operator_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/minio/console/models" +) + +// GetAllocatableResourcesOKCode is the HTTP code returned for type GetAllocatableResourcesOK +const GetAllocatableResourcesOKCode int = 200 + +/*GetAllocatableResourcesOK A successful response. + +swagger:response getAllocatableResourcesOK +*/ +type GetAllocatableResourcesOK struct { + + /* + In: Body + */ + Payload *models.AllocatableResourcesResponse `json:"body,omitempty"` +} + +// NewGetAllocatableResourcesOK creates GetAllocatableResourcesOK with default headers values +func NewGetAllocatableResourcesOK() *GetAllocatableResourcesOK { + + return &GetAllocatableResourcesOK{} +} + +// WithPayload adds the payload to the get allocatable resources o k response +func (o *GetAllocatableResourcesOK) WithPayload(payload *models.AllocatableResourcesResponse) *GetAllocatableResourcesOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get allocatable resources o k response +func (o *GetAllocatableResourcesOK) SetPayload(payload *models.AllocatableResourcesResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAllocatableResourcesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*GetAllocatableResourcesDefault Generic error response. + +swagger:response getAllocatableResourcesDefault +*/ +type GetAllocatableResourcesDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewGetAllocatableResourcesDefault creates GetAllocatableResourcesDefault with default headers values +func NewGetAllocatableResourcesDefault(code int) *GetAllocatableResourcesDefault { + if code <= 0 { + code = 500 + } + + return &GetAllocatableResourcesDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the get allocatable resources default response +func (o *GetAllocatableResourcesDefault) WithStatusCode(code int) *GetAllocatableResourcesDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the get allocatable resources default response +func (o *GetAllocatableResourcesDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the get allocatable resources default response +func (o *GetAllocatableResourcesDefault) WithPayload(payload *models.Error) *GetAllocatableResourcesDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get allocatable resources default response +func (o *GetAllocatableResourcesDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetAllocatableResourcesDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/operatorapi/operations/operator_api/get_allocatable_resources_urlbuilder.go b/operatorapi/operations/operator_api/get_allocatable_resources_urlbuilder.go new file mode 100644 index 000000000..7adf9b389 --- /dev/null +++ b/operatorapi/operations/operator_api/get_allocatable_resources_urlbuilder.go @@ -0,0 +1,119 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package operator_api + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + + "github.com/go-openapi/swag" +) + +// GetAllocatableResourcesURL generates an URL for the get allocatable resources operation +type GetAllocatableResourcesURL struct { + NumNodes int32 + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAllocatableResourcesURL) WithBasePath(bp string) *GetAllocatableResourcesURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetAllocatableResourcesURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetAllocatableResourcesURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/cluster/allocatable-resources" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + numNodesQ := swag.FormatInt32(o.NumNodes) + if numNodesQ != "" { + qs.Set("num_nodes", numNodesQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetAllocatableResourcesURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetAllocatableResourcesURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetAllocatableResourcesURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetAllocatableResourcesURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetAllocatableResourcesURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetAllocatableResourcesURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/operatorapi/operator_nodes.go b/operatorapi/operator_nodes.go index a0a013ed8..c137b936b 100644 --- a/operatorapi/operator_nodes.go +++ b/operatorapi/operator_nodes.go @@ -18,6 +18,7 @@ package operatorapi import ( "context" + "errors" "sort" "github.com/minio/minio-go/v7/pkg/set" @@ -50,6 +51,20 @@ func registerNodesHandlers(api *operations.OperatorAPI) { } return operator_api.NewListNodeLabelsOK().WithPayload(*resp) }) + + api.OperatorAPIGetAllocatableResourcesHandler = operator_api.GetAllocatableResourcesHandlerFunc(func(params operator_api.GetAllocatableResourcesParams, principal *models.Principal) middleware.Responder { + resp, err := getAllocatableResourcesResponse(params.NumNodes, principal) + if err != nil { + return operator_api.NewGetAllocatableResourcesDefault(int(err.Code)).WithPayload(err) + } + return operator_api.NewGetAllocatableResourcesOK().WithPayload(resp) + }) +} + +type NodeResourceInfo struct { + Name string + AllocatableMemory int64 + AllocatableCPU int64 } // getMaxAllocatableMemory get max allocatable memory given a desired number of nodes @@ -211,3 +226,147 @@ func getNodeLabelsResponse(ctx context.Context, session *models.Principal) (*mod } return clusterResources, nil } + +func getClusterResourcesInfo(numNodes int32, inNodesResources []NodeResourceInfo) *models.AllocatableResourcesResponse { + + // purge any nodes with 0 cpu + var nodesResources []NodeResourceInfo + for _, n := range inNodesResources { + if n.AllocatableCPU > 0 { + nodesResources = append(nodesResources, n) + } + } + + if int32(len(nodesResources)) < numNodes || numNodes == 0 { + return &models.AllocatableResourcesResponse{ + CPUPriority: &models.NodeMaxAllocatableResources{ + MaxAllocatableCPU: 0, + MaxAllocatableMem: 0, + }, + MemPriority: &models.NodeMaxAllocatableResources{ + MaxAllocatableCPU: 0, + MaxAllocatableMem: 0, + }, + MinAllocatableCPU: 0, + MinAllocatableMem: 0, + } + } + + allocatableResources := &models.AllocatableResourcesResponse{} + + // sort nodesResources giving CPU priority + sort.Slice(nodesResources, func(i, j int) bool { return nodesResources[i].AllocatableCPU < nodesResources[j].AllocatableCPU }) + maxCPUNodesNeeded := len(nodesResources) - int(numNodes) + maxMemNodesNeeded := maxCPUNodesNeeded + + maxAllocatableCPU := nodesResources[maxCPUNodesNeeded].AllocatableCPU + minAllocatableCPU := nodesResources[maxCPUNodesNeeded].AllocatableCPU + minAllocatableMem := nodesResources[maxMemNodesNeeded].AllocatableMemory + + availableMemsForMaxCPU := []int64{} + for _, info := range nodesResources { + if info.AllocatableCPU >= maxAllocatableCPU { + availableMemsForMaxCPU = append(availableMemsForMaxCPU, info.AllocatableMemory) + } + // min allocatable resources overall + minAllocatableCPU = min(minAllocatableCPU, info.AllocatableCPU) + minAllocatableMem = min(minAllocatableMem, info.AllocatableMemory) + } + + sort.Slice(availableMemsForMaxCPU, func(i, j int) bool { return availableMemsForMaxCPU[i] < availableMemsForMaxCPU[j] }) + maxAllocatableMem := availableMemsForMaxCPU[len(availableMemsForMaxCPU)-int(numNodes)] + + allocatableResources.MinAllocatableCPU = minAllocatableCPU + allocatableResources.MinAllocatableMem = minAllocatableMem + allocatableResources.CPUPriority = &models.NodeMaxAllocatableResources{ + MaxAllocatableCPU: maxAllocatableCPU, + MaxAllocatableMem: maxAllocatableMem, + } + + // sort nodesResources giving Mem priority + sort.Slice(nodesResources, func(i, j int) bool { return nodesResources[i].AllocatableMemory < nodesResources[j].AllocatableMemory }) + maxMemNodesNeeded = len(nodesResources) - int(numNodes) + maxAllocatableMem = nodesResources[maxMemNodesNeeded].AllocatableMemory + + availableCPUsForMaxMem := []int64{} + for _, info := range nodesResources { + if info.AllocatableMemory >= maxAllocatableMem { + availableCPUsForMaxMem = append(availableCPUsForMaxMem, info.AllocatableCPU) + } + } + + sort.Slice(availableCPUsForMaxMem, func(i, j int) bool { return availableCPUsForMaxMem[i] < availableCPUsForMaxMem[j] }) + maxAllocatableCPU = availableCPUsForMaxMem[len(availableCPUsForMaxMem)-int(numNodes)] + + allocatableResources.MemPriority = &models.NodeMaxAllocatableResources{ + MaxAllocatableCPU: maxAllocatableCPU, + MaxAllocatableMem: maxAllocatableMem, + } + + return allocatableResources +} + +// getAllocatableResources get max allocatable memory given a desired number of nodes +func getAllocatableResources(ctx context.Context, clientset v1.CoreV1Interface, numNodes int32) (*models.AllocatableResourcesResponse, error) { + if numNodes == 0 { + return nil, errors.New("error NumNodes must be greated than 0") + } + + // get all nodes from cluster + nodes, err := clientset.Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + nodesInfo := []NodeResourceInfo{} +OUTER: + for _, n := range nodes.Items { + // Don't consider node if it has a NoSchedule or NoExecute Taint + for _, t := range n.Spec.Taints { + switch t.Effect { + case corev1.TaintEffectNoSchedule: + continue OUTER + case corev1.TaintEffectNoExecute: + continue OUTER + default: + continue + } + } + + var nodeMemory int64 + var nodeCPU int64 + if quantity, ok := n.Status.Allocatable[corev1.ResourceMemory]; ok { + // availableMemSizes = append(availableMemSizes, quantity.Value()) + nodeMemory = quantity.Value() + } + // we assume all nodes have allocatable cpu resource + if quantity, ok := n.Status.Allocatable[corev1.ResourceCPU]; ok { + // availableCPU = append(availableCPU, quantity.Value()) + nodeCPU = quantity.Value() + } + nodeInfo := NodeResourceInfo{ + Name: n.Name, + AllocatableCPU: nodeCPU, + AllocatableMemory: nodeMemory, + } + nodesInfo = append(nodesInfo, nodeInfo) + } + res := getClusterResourcesInfo(numNodes, nodesInfo) + + return res, nil +} + +// Get allocatable resources response + +func getAllocatableResourcesResponse(numNodes int32, session *models.Principal) (*models.AllocatableResourcesResponse, *models.Error) { + ctx := context.Background() + client, err := cluster.K8sClient(session.STSSessionToken) + if err != nil { + return nil, prepareError(err) + } + + clusterResources, err := getAllocatableResources(ctx, client.CoreV1(), numNodes) + if err != nil { + return nil, prepareError(err) + } + return clusterResources, nil +} diff --git a/portal-ui/src/common/types.ts b/portal-ui/src/common/types.ts index 401e9f7d8..3accaa8b3 100644 --- a/portal-ui/src/common/types.ts +++ b/portal-ui/src/common/types.ts @@ -153,10 +153,12 @@ export interface IResourceModel { export interface IResourceRequests { memory: number; + cpu: number; } export interface IResourceLimits { memory: number; + cpu: number; } export interface ITLSTenantConfiguration { diff --git a/portal-ui/src/common/utils.ts b/portal-ui/src/common/utils.ts index 5bee31d34..4369b9c33 100644 --- a/portal-ui/src/common/utils.ts +++ b/portal-ui/src/common/utils.ts @@ -15,8 +15,10 @@ // along with this program. If not, see . import storage from "local-storage-fallback"; +import get from "lodash/get"; import { ICapacity, IErasureCodeCalc, IStorageFactors } from "./types"; import { IPool } from "../screens/Console/Tenants/ListTenants/types"; +import { AllocableResourcesResponse } from "../screens/Console/Tenants/types"; const minStReq = 1073741824; // Minimal Space required for MinIO const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes @@ -594,3 +596,113 @@ export const decodeFileName = (text: string) => { return text; } }; + +export const setResourcesValidation = ( + memorySize: number, + cpusSelected: number, + maxAllocatableResources: AllocableResourcesResponse +) => { + const requestedSizeBytes = getBytes(memorySize.toString(10), "GB"); + const memReqSize = parseInt(requestedSizeBytes, 10); + + const minimalRequiredMemory = 2147483648; // Minimal required memory, 2Gi + + const memoryExists = get( + maxAllocatableResources, + "min_allocatable_mem", + false + ); + + const cpuExists = get(maxAllocatableResources, "min_allocatable_cpu", false); + + if (memoryExists === false) { + return { + error: + "No available memory for the selected number of nodes. Please try another combination.", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if (cpuExists === false) { + return { + error: + "No available CPUs for the selected number of nodes. Please try another combination", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if (memReqSize < minimalRequiredMemory) { + return { + error: "Memory size is set bellow minimum required", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if (cpusSelected < 1) { + return { + error: "CPU amount is set bellow minimum available", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if ( + memReqSize <= maxAllocatableResources.mem_priority.max_allocatable_mem && + cpusSelected > maxAllocatableResources.mem_priority.max_allocatable_cpu + ) { + return { + error: + "It is not possible to allocate this amount of memory in all the CPUs", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if ( + cpusSelected <= maxAllocatableResources.cpu_priority.max_allocatable_cpu && + memReqSize > maxAllocatableResources.cpu_priority.max_allocatable_mem + ) { + return { + error: + "It is not possible to allocate this amount of CPUs with the available memory", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + if ( + cpusSelected > maxAllocatableResources.cpu_priority.max_allocatable_cpu || + memReqSize > maxAllocatableResources.mem_priority.max_allocatable_mem + ) { + return { + error: "CPUs or Memory selected is beyond bounds", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }; + } + + return { + error: "", + memoryRequest: memReqSize, + memoryLimit: maxAllocatableResources.mem_priority.max_allocatable_mem, + cpuRequest: cpusSelected, + cpuLimit: maxAllocatableResources.cpu_priority.max_allocatable_cpu, + }; +}; diff --git a/portal-ui/src/screens/Console/Common/FormComponents/InputBoxWrapper/InputBoxWrapper.tsx b/portal-ui/src/screens/Console/Common/FormComponents/InputBoxWrapper/InputBoxWrapper.tsx index 8088d1be8..cc36ab6be 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/InputBoxWrapper/InputBoxWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/InputBoxWrapper/InputBoxWrapper.tsx @@ -58,6 +58,7 @@ interface InputBoxProps { overlayObject?: any; extraInputProps?: StandardInputProps["inputProps"]; noLabelMinWidth?: boolean; + pattern?: string; } const styles = (theme: Theme) => @@ -121,6 +122,7 @@ const InputBoxWrapper = ({ extraInputProps = {}, overlayAction, noLabelMinWidth = false, + pattern="", classes, }: InputBoxProps) => { let inputProps: any = { "data-index": index, ...extraInputProps }; @@ -133,6 +135,10 @@ const InputBoxWrapper = ({ inputProps["max"] = max; } + if(pattern !== "") { + inputProps["pattern"] = pattern; + } + return ( . -import React, { Fragment, useCallback, useEffect, useState } from "react"; +import React, { Fragment } from "react"; import { connect } from "react-redux"; import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; @@ -29,22 +29,11 @@ import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; -import { - calculateDistribution, - erasureCodeCalc, - getBytes, - niceBytes, - setMemoryResource, -} from "../../../../../common/utils"; -import { ecListTransform, Opts } from "../../ListTenants/utils"; -import { IMemorySize } from "../../ListTenants/types"; -import { - ErrorResponseHandler, - ICapacity, - IErasureCodeCalc, -} from "../../../../../common/types"; -import { commonFormValidation } from "../../../../../utils/validationFunctions"; -import api from "../../../../../common/api"; +import { niceBytes } from "../../../../../common/utils"; +import { Opts } from "../../ListTenants/utils"; +import { IResourcesSize } from "../../ListTenants/types"; +import { IErasureCodeCalc } from "../../../../../common/types"; + import { Divider } from "@mui/material"; interface ISizePreviewProps { @@ -61,11 +50,12 @@ interface ISizePreviewProps { ecParityChoices: Opts[]; cleanECChoices: string[]; maxAllocableMemo: number; - memorySize: IMemorySize; + resourcesSize: IResourcesSize; distribution: any; ecParityCalc: IErasureCodeCalc; limitSize: any; selectedStorageClass: string; + cpuToUse: string; } const styles = (theme: Theme) => @@ -96,201 +86,17 @@ const SizePreview = ({ ecParityChoices, cleanECChoices, maxAllocableMemo, - memorySize, + resourcesSize, distribution, ecParityCalc, limitSize, selectedStorageClass, + cpuToUse, }: ISizePreviewProps) => { - const [errorFlag, setErrorFlag] = useState(false); - const [nodeError, setNodeError] = useState(""); const usableInformation = ecParityCalc.storageFactors.find( (element) => element.erasureCode === ecParity ); - // Common - const updateField = useCallback( - (field: string, value: any) => { - updateAddField("tenantSize", field, value); - }, - [updateAddField] - ); - - /*Debounce functions*/ - - // Storage Quotas - - const validateMemorySize = useCallback(() => { - const memSize = parseInt(memoryNode) || 0; - const clusterSize = volumeSize || 0; - const maxMemSize = maxAllocableMemo || 0; - const clusterSizeFactor = sizeFactor; - - const clusterSizeBytes = getBytes( - clusterSize.toString(10), - clusterSizeFactor - ); - const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize); - updateField("memorySize", memoSize); - }, [maxAllocableMemo, memoryNode, sizeFactor, updateField, volumeSize]); - - const getMaxAllocableMemory = (nodes: string) => { - if (nodes !== "" && !isNaN(parseInt(nodes))) { - setNodeError(""); - api - .invoke( - "GET", - `/api/v1/cluster/max-allocatable-memory?num_nodes=${nodes}` - ) - .then((res: { max_memory: number }) => { - const maxMemory = res.max_memory ? res.max_memory : 0; - updateField("maxAllocableMemo", maxMemory); - }) - .catch((err: ErrorResponseHandler) => { - setErrorFlag(true); - setNodeError(err.errorMessage); - }); - } - }; - - useEffect(() => { - validateMemorySize(); - }, [memoryNode, validateMemorySize]); - - useEffect(() => { - validateMemorySize(); - }, [maxAllocableMemo, validateMemorySize]); - - useEffect(() => { - if (ecParityChoices.length > 0 && distribution.error === "") { - const ecCodeValidated = erasureCodeCalc( - cleanECChoices, - distribution.persistentVolumes, - distribution.pvSize, - distribution.nodes - ); - - updateField("ecParityCalc", ecCodeValidated); - updateField("ecParity", ecCodeValidated.defaultEC); - } - }, [ecParityChoices.length, distribution, cleanECChoices, updateField]); - /*End debounce functions*/ - - /*Calculate Allocation*/ - useEffect(() => { - validateClusterSize(); - getECValue(); - getMaxAllocableMemory(nodes); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodes, volumeSize, sizeFactor, drivesPerServer]); - - const validateClusterSize = () => { - const size = volumeSize; - const factor = sizeFactor; - const limitSize = getBytes("12", "Ti", true); - - const clusterCapacity: ICapacity = { - unit: factor, - value: size.toString(), - }; - - const distrCalculate = calculateDistribution( - clusterCapacity, - parseInt(nodes), - parseInt(limitSize), - parseInt(drivesPerServer) - ); - - updateField("distribution", distrCalculate); - }; - - const getECValue = () => { - updateField("ecParity", ""); - - if (nodes.trim() !== "" && drivesPerServer.trim() !== "") { - api - .invoke("GET", `/api/v1/get-parity/${nodes}/${drivesPerServer}`) - .then((ecList: string[]) => { - updateField("ecParityChoices", ecListTransform(ecList)); - updateField("cleanECChoices", ecList); - }) - .catch((err: ErrorResponseHandler) => { - updateField("ecparityChoices", []); - isPageValid("tenantSize", false); - updateField("ecParity", ""); - }); - } - }; - /*Calculate Allocation End*/ - - /* Validations of pages */ - - useEffect(() => { - const parsedSize = getBytes(volumeSize, sizeFactor, true); - const commonValidation = commonFormValidation([ - { - fieldKey: "nodes", - required: true, - value: nodes, - customValidation: errorFlag, - customValidationMessage: nodeError, - }, - { - fieldKey: "volume_size", - required: true, - value: volumeSize, - customValidation: - parseInt(parsedSize) < 1073741824 || - parseInt(parsedSize) > limitSize[selectedStorageClass], - customValidationMessage: `Volume size must be greater than 1Gi and less than ${niceBytes( - limitSize[selectedStorageClass], - true - )}`, - }, - { - fieldKey: "memory_per_node", - required: true, - value: memoryNode, - customValidation: parseInt(memoryNode) < 2, - customValidationMessage: "Memory size must be greater than 2Gi", - }, - { - fieldKey: "drivesps", - required: true, - value: drivesPerServer, - customValidation: parseInt(drivesPerServer) < 1, - customValidationMessage: "There must be at least one drive", - }, - ]); - - isPageValid( - "tenantSize", - !("nodes" in commonValidation) && - !("volume_size" in commonValidation) && - !("memory_per_node" in commonValidation) && - !("drivesps" in commonValidation) && - distribution.error === "" && - ecParityCalc.error === 0 && - memorySize.error === "" - ); - }, [ - nodes, - volumeSize, - sizeFactor, - memoryNode, - distribution, - drivesPerServer, - ecParityCalc, - memorySize, - limitSize, - selectedStorageClass, - isPageValid, - errorFlag, - nodeError, - ]); - - /* End Validation of pages */ - return (

Resource Allocation

@@ -327,6 +133,10 @@ const SizePreview = ({ {memoryNode} Gi )} + + CPU Selection + {cpuToUse} + {ecParityCalc.error === 0 && usableInformation && ( @@ -390,12 +200,13 @@ const mapState = (state: AppState) => ({ cleanECChoices: state.tenants.createTenant.fields.tenantSize.cleanECChoices, maxAllocableMemo: state.tenants.createTenant.fields.tenantSize.maxAllocableMemo, - memorySize: state.tenants.createTenant.fields.tenantSize.memorySize, + resourcesSize: state.tenants.createTenant.fields.tenantSize.resourcesSize, distribution: state.tenants.createTenant.fields.tenantSize.distribution, ecParityCalc: state.tenants.createTenant.fields.tenantSize.ecParityCalc, limitSize: state.tenants.createTenant.fields.tenantSize.limitSize, selectedStorageClass: state.tenants.createTenant.fields.nameTenant.selectedStorageClass, + cpuToUse: state.tenants.createTenant.fields.tenantSize.cpuToUse, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx index aadbe56ab..b7dd5eb82 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx @@ -17,8 +17,10 @@ import React, { Fragment, useCallback, useEffect, useState } from "react"; import { connect } from "react-redux"; import { Theme } from "@mui/material/styles"; +import { SelectChangeEvent } from "@mui/material"; import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; +import get from "lodash/get"; import { AppState } from "../../../../../store"; import { isPageValid, updateAddField } from "../../actions"; import { @@ -33,21 +35,17 @@ import { getBytes, k8sfactorForDropdown, niceBytes, - setMemoryResource, + setResourcesValidation, } from "../../../../../common/utils"; import { clearValidationError } from "../../utils"; import { ecListTransform, Opts } from "../../ListTenants/utils"; -import { IMemorySize } from "../../ListTenants/types"; -import { - ErrorResponseHandler, - ICapacity, - IErasureCodeCalc, -} from "../../../../../common/types"; +import { IResourcesSize } from "../../ListTenants/types"; +import { AllocableResourcesResponse } from "../../types"; +import { ICapacity, IErasureCodeCalc } from "../../../../../common/types"; import { commonFormValidation } from "../../../../../utils/validationFunctions"; import api from "../../../../../common/api"; import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper"; -import { SelectChangeEvent } from "@mui/material"; interface ITenantSizeProps { classes: any; @@ -63,11 +61,15 @@ interface ITenantSizeProps { ecParityChoices: Opts[]; cleanECChoices: string[]; maxAllocableMemo: number; - memorySize: IMemorySize; + resourcesSize: IResourcesSize; distribution: any; ecParityCalc: IErasureCodeCalc; limitSize: any; selectedStorageClass: string; + cpuToUse: string; + maxAllocatableResources: AllocableResourcesResponse; + maxCPUsUse: string; + maxMemorySize: string; } const styles = (theme: Theme) => @@ -106,11 +108,15 @@ const TenantSize = ({ ecParityChoices, cleanECChoices, maxAllocableMemo, - memorySize, + resourcesSize, distribution, ecParityCalc, limitSize, + cpuToUse, selectedStorageClass, + maxAllocatableResources, + maxCPUsUse, + maxMemorySize, }: ITenantSizeProps) => { const [validationErrors, setValidationErrors] = useState({}); const [errorFlag, setErrorFlag] = useState(false); @@ -132,46 +138,22 @@ const TenantSize = ({ // Storage Quotas - const validateMemorySize = useCallback(() => { - const memSize = parseInt(memoryNode) || 0; - const clusterSize = volumeSize || 0; - const maxMemSize = maxAllocableMemo || 0; - const clusterSizeFactor = sizeFactor; + const validateResourcesSize = useCallback(() => { + const memSize = memoryNode || "0"; + const cpusSelected = cpuToUse; - const clusterSizeBytes = getBytes( - clusterSize.toString(10), - clusterSizeFactor + const resourcesSize = setResourcesValidation( + parseInt(memSize), + parseInt(cpusSelected), + maxAllocatableResources ); - const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize); - updateField("memorySize", memoSize); - }, [maxAllocableMemo, memoryNode, sizeFactor, updateField, volumeSize]); - const getMaxAllocableMemory = (nodes: string) => { - if (nodes !== "" && !isNaN(parseInt(nodes))) { - setNodeError(""); - api - .invoke( - "GET", - `/api/v1/cluster/max-allocatable-memory?num_nodes=${nodes}` - ) - .then((res: { max_memory: number }) => { - const maxMemory = res.max_memory ? res.max_memory : 0; - updateField("maxAllocableMemo", maxMemory); - }) - .catch((err: ErrorResponseHandler) => { - setErrorFlag(true); - setNodeError(err.errorMessage); - }); - } - }; + updateField("resourcesSize", resourcesSize); + }, [memoryNode, cpuToUse, maxAllocatableResources, updateField]); useEffect(() => { - validateMemorySize(); - }, [memoryNode, validateMemorySize]); - - useEffect(() => { - validateMemorySize(); - }, [maxAllocableMemo, validateMemorySize]); + validateResourcesSize(); + }, [memoryNode, cpuToUse, validateResourcesSize]); useEffect(() => { if (ecParityChoices.length > 0 && distribution.error === "") { @@ -190,13 +172,7 @@ const TenantSize = ({ /*Calculate Allocation*/ useEffect(() => { - validateClusterSize(); - getECValue(); - getMaxAllocableMemory(nodes); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodes, volumeSize, sizeFactor, drivesPerServer]); - - const validateClusterSize = () => { + //Validate Cluster Size const size = volumeSize; const factor = sizeFactor; const limitSize = getBytes("12", "Ti", true); @@ -214,31 +190,84 @@ const TenantSize = ({ ); updateField("distribution", distrCalculate); - }; + setErrorFlag(false); + setNodeError(""); - const getECValue = () => { - updateField("ecParity", ""); + // Get allocatable Resources + api + .invoke("GET", `api/v1/cluster/allocatable-resources?num_nodes=${nodes}`) + .then((res: AllocableResourcesResponse) => { + updateField("maxAllocatableResources", res); + + const maxAllocatableResources = res; + + const memoryExists = get( + maxAllocatableResources, + "min_allocatable_mem", + false + ); + + const cpuExists = get( + maxAllocatableResources, + "min_allocatable_cpu", + false + ); + + if (memoryExists === false || cpuExists === false) { + updateField("cpuToUse", 0); + + updateField("maxMemorySize", "0"); + updateField("maxCPUsUse", "0"); + + validateResourcesSize(); + return; + } + + // We default to Best CPU Configuration + updateField( + "maxMemorySize", + res.mem_priority.max_allocatable_mem.toString() + ); + updateField( + "maxCPUsUse", + res.cpu_priority.max_allocatable_cpu.toString() + ); + + updateField("maxAllocableMemo", res.mem_priority.max_allocatable_mem); + + const cpuInt = parseInt(cpuToUse); + const maxAlocatableCPU = get( + maxAllocatableResources, + "cpu_priority.max_allocatable_cpu", + 0 + ); + + if (cpuInt === 0 && cpuInt !== maxAlocatableCPU) { + updateField("cpuToUse", maxAlocatableCPU); + } else if (cpuInt > maxAlocatableCPU) { + updateField("cpuToUse", maxAlocatableCPU); + } + + // We reset error states + validateResourcesSize(); + }) + .catch((err: any) => { + updateField("maxAllocableMemo", 0); + updateField("cpuToUse", "0"); + setErrorFlag(true); + setNodeError(err.errorMessage); + console.error(err); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodes, volumeSize, sizeFactor, updateField]); - if (nodes.trim() !== "" && drivesPerServer.trim() !== "") { - api - .invoke("GET", `/api/v1/get-parity/${nodes}/${drivesPerServer}`) - .then((ecList: string[]) => { - updateField("ecParityChoices", ecListTransform(ecList)); - updateField("cleanECChoices", ecList); - }) - .catch((err: ErrorResponseHandler) => { - updateField("ecparityChoices", []); - isPageValid("tenantSize", false); - updateField("ecParity", ""); - }); - } - }; /*Calculate Allocation End*/ /* Validations of pages */ useEffect(() => { const parsedSize = getBytes(volumeSize, sizeFactor, true); + const commonValidation = commonFormValidation([ { fieldKey: "nodes", @@ -283,7 +312,10 @@ const TenantSize = ({ !("drivesps" in commonValidation) && distribution.error === "" && ecParityCalc.error === 0 && - memorySize.error === "" + resourcesSize.error === "" && + parseInt(cpuToUse) <= parseInt(maxCPUsUse) && + parseInt(cpuToUse) > 0 && + ecParity !== "" ); setValidationErrors(commonValidation); @@ -293,16 +325,40 @@ const TenantSize = ({ sizeFactor, memoryNode, distribution, - drivesPerServer, ecParityCalc, - memorySize, + resourcesSize, limitSize, selectedStorageClass, + cpuToUse, + maxCPUsUse, isPageValid, errorFlag, nodeError, + drivesPerServer, + ecParity, ]); + useEffect(() => { + if (distribution.error === "") { + // Get EC Value + updateField("ecParity", ""); + + if (nodes.trim() !== "" && distribution.disks !== 0) { + api + .invoke("GET", `api/v1/get-parity/${nodes}/${distribution.disks}`) + .then((ecList: string[]) => { + updateField("ecParityChoices", ecListTransform(ecList)); + updateField("cleanECChoices", ecList); + }) + .catch((err: any) => { + updateField("ecparityChoices", []); + isPageValid("tenantSize", false); + updateField("ecParity", ""); + }); + } + } + }, [distribution, isPageValid, updateField, nodes]); + /* End Validation of pages */ return ( @@ -320,19 +376,20 @@ const TenantSize = ({
{distribution.error}
)} - {memorySize.error !== "" && ( + {resourcesSize.error !== "" && ( -
{memorySize.error}
+
{resourcesSize.error}
)} ) => { - updateField("nodes", e.target.value); - cleanValidation("nodes"); + if (e.target.validity.valid) { + updateField("nodes", e.target.value); + cleanValidation("nodes"); + } }} label="Number of Servers" disabled={selectedStorageClass === ""} @@ -340,16 +397,18 @@ const TenantSize = ({ min="4" required error={validationErrors["nodes"] || ""} + pattern={"[0-9]*"} /> ) => { - updateField("drivesPerServer", e.target.value); - cleanValidation("drivesps"); + if (e.target.validity.valid) { + updateField("drivesPerServer", e.target.value); + cleanValidation("drivesps"); + } }} label="Number of Drives per Server" value={drivesPerServer} @@ -357,6 +416,7 @@ const TenantSize = ({ min="1" required error={validationErrors["drivesps"] || ""} + pattern={"[0-9]*"} /> @@ -396,42 +456,66 @@ const TenantSize = ({
- - - ) => { - updateField("memoryNode", e.target.value); - cleanValidation("memory_per_node"); - }} - label="Memory per Node [Gi]" - value={memoryNode} - disabled={selectedStorageClass === ""} - required - error={validationErrors["memory_per_node"] || ""} - min="2" - /> - - - ) => { - updateField("ecParity", e.target.value as string); - }} - label="Erasure Code Parity" - disabled={selectedStorageClass === ""} - value={ecParity} - options={ecParityChoices} - /> - - Please select the desired parity. This setting will change the max - usable capacity in the cluster - - - + + { + if (e.target.validity.valid) { + updateField("cpuToUse", e.target.value); + } + }} + value={cpuToUse} + disabled={selectedStorageClass === ""} + min="1" + max={maxCPUsUse} + error={ + parseInt(cpuToUse) > parseInt(maxCPUsUse) || + parseInt(cpuToUse) <= 0 || + isNaN(parseInt(cpuToUse)) + ? "Invalid CPU Configuration" + : "" + } + pattern={"[0-9]*"} + /> + + + + ) => { + updateField("memoryNode", e.target.value); + cleanValidation("memory_per_node"); + }} + label="Memory per Node [Gi]" + value={memoryNode} + disabled={selectedStorageClass === ""} + required + error={validationErrors["memory_per_node"] || ""} + min="2" + max={maxMemorySize} + /> + + + ) => { + updateField("ecParity", e.target.value as string); + }} + label="Erasure Code Parity" + disabled={selectedStorageClass === ""} + value={ecParity} + options={ecParityChoices} + /> + + Please select the desired parity. This setting will change the max + usable capacity in the cluster + + ); }; @@ -448,12 +532,17 @@ const mapState = (state: AppState) => ({ cleanECChoices: state.tenants.createTenant.fields.tenantSize.cleanECChoices, maxAllocableMemo: state.tenants.createTenant.fields.tenantSize.maxAllocableMemo, - memorySize: state.tenants.createTenant.fields.tenantSize.memorySize, + resourcesSize: state.tenants.createTenant.fields.tenantSize.resourcesSize, distribution: state.tenants.createTenant.fields.tenantSize.distribution, ecParityCalc: state.tenants.createTenant.fields.tenantSize.ecParityCalc, - limitSize: state.tenants.createTenant.fields.tenantSize.limitSize, + limitSize: state.tenants.createTenant.limitSize, selectedStorageClass: state.tenants.createTenant.fields.nameTenant.selectedStorageClass, + cpuToUse: state.tenants.createTenant.fields.tenantSize.cpuToUse, + maxAllocatableResources: + state.tenants.createTenant.fields.tenantSize.maxAllocatableResources, + maxCPUsUse: state.tenants.createTenant.fields.tenantSize.maxCPUsUse, + maxMemorySize: state.tenants.createTenant.fields.tenantSize.maxMemorySize, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts index 02bb4c2c0..b822cf79f 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts @@ -122,8 +122,10 @@ export interface ITenantsResponse { tenants: ITenant[]; } -export interface IMemorySize { +export interface IResourcesSize { error: string; - limit: number; - request: number; + memoryRequest: number; + memoryLimit: number; + cpuRequest: number; + cpuLimit: number; } diff --git a/portal-ui/src/screens/Console/Tenants/reducer.ts b/portal-ui/src/screens/Console/Tenants/reducer.ts index 2b311c5ac..071b1e33b 100644 --- a/portal-ui/src/screens/Console/Tenants/reducer.ts +++ b/portal-ui/src/screens/Console/Tenants/reducer.ts @@ -39,6 +39,7 @@ import { ADD_TENANT_ENCRYPTION_VAULT_CA, ADD_TENANT_ENCRYPTION_GEMALTO_CA, ADD_TENANT_RESET_FORM, + ADD_TENANT_SET_LIMIT_SIZE, TENANT_DETAILS_SET_LOADING, TENANT_DETAILS_SET_CURRENT_TENANT, TENANT_DETAILS_SET_TENANT, @@ -200,10 +201,13 @@ const initialState: ITenantState = { ecParityChoices: [], cleanECChoices: [], maxAllocableMemo: 0, - memorySize: { + cpuToUse: "0", + resourcesSize: { error: "", - limit: 0, - request: 0, + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, }, distribution: { error: "", @@ -221,6 +225,20 @@ const initialState: ITenantState = { storageFactors: [], }, limitSize: {}, + maxAllocatableResources: { + min_allocatable_mem: 0, + min_allocatable_cpu: 0, + cpu_priority: { + max_allocatable_cpu: 0, + max_allocatable_mem: 0, + }, + mem_priority: { + max_allocatable_cpu: 0, + max_allocatable_mem: 0, + }, + }, + maxCPUsUse: "0", + maxMemorySize: "0" }, affinity: { nodeSelectorLabels: "", @@ -371,6 +389,13 @@ export function tenantsReducer( }, }; return { ...changeCL }; + case ADD_TENANT_SET_LIMIT_SIZE: + const changeSizeLimit = { + ...state, + createTenant: { ...state.createTenant, limitSize: action.limitSize }, + }; + + return { ...changeSizeLimit }; case ADD_TENANT_ADD_MINIO_KEYPAIR: const minioCerts = [ ...state.createTenant.certificates.minioCertificates, @@ -714,11 +739,6 @@ export function tenantsReducer( ecParityChoices: [], cleanECChoices: [], maxAllocableMemo: 0, - memorySize: { - error: "", - limit: 0, - request: 0, - }, distribution: { error: "", nodes: 0, @@ -735,6 +755,28 @@ export function tenantsReducer( storageFactors: [], }, limitSize: {}, + cpuToUse: "0", + resourcesSize: { + error: "", + memoryRequest: 0, + memoryLimit: 0, + cpuRequest: 0, + cpuLimit: 0, + }, + maxAllocatableResources: { + min_allocatable_mem: 0, + min_allocatable_cpu: 0, + cpu_priority: { + max_allocatable_cpu: 0, + max_allocatable_mem: 0, + }, + mem_priority: { + max_allocatable_cpu: 0, + max_allocatable_mem: 0, + }, + }, + maxCPUsUse: "0", + maxMemorySize: "0" }, affinity: { nodeSelectorLabels: "", diff --git a/portal-ui/src/screens/Console/Tenants/types.ts b/portal-ui/src/screens/Console/Tenants/types.ts index 9ba82861d..af0165eb4 100644 --- a/portal-ui/src/screens/Console/Tenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/types.ts @@ -15,7 +15,7 @@ // along with this program. If not, see . import { IErasureCodeCalc } from "../../../common/types"; -import { IMemorySize, ITenant } from "./ListTenants/types"; +import { ITenant, IResourcesSize } from "./ListTenants/types"; import { KeyPair, Opts } from "./ListTenants/utils"; export const ADD_TENANT_SET_CURRENT_PAGE = "ADD_TENANT/SET_CURRENT_PAGE"; @@ -238,10 +238,14 @@ export interface ITenantSizeFields { ecParityChoices: Opts[]; cleanECChoices: string[]; maxAllocableMemo: number; - memorySize: IMemorySize; + resourcesSize: IResourcesSize; distribution: any; ecParityCalc: IErasureCodeCalc; + cpuToUse: string; limitSize: any; + maxAllocatableResources: AllocableResourcesResponse; + maxCPUsUse: string; + maxMemorySize: string; } export interface ITenantAffinity { @@ -268,6 +272,18 @@ export interface ILabelKeyPair { labelValue: string; } +export interface AllocableResourcesResponse { + min_allocatable_mem?: number; + min_allocatable_cpu?: number; + cpu_priority: NodeMaxAllocatableResources; + mem_priority: NodeMaxAllocatableResources; +} + +export interface NodeMaxAllocatableResources { + max_allocatable_cpu: number; + max_allocatable_mem: number; +} + interface SetTenantWizardPage { type: typeof ADD_TENANT_SET_CURRENT_PAGE; page: number; diff --git a/swagger-operator.yml b/swagger-operator.yml index 0225b43af..f9bc8c174 100644 --- a/swagger-operator.yml +++ b/swagger-operator.yml @@ -19,7 +19,7 @@ securityDefinitions: tokenUrl: http://min.io # Apply the key security definition to all APIs security: - - key: [ ] + - key: [] paths: /login: get: @@ -35,7 +35,7 @@ paths: schema: $ref: "#/definitions/error" # Exclude this API from the authentication requirement - security: [ ] + security: [] tags: - UserAPI /login/operator: @@ -55,7 +55,7 @@ paths: description: Generic error response. schema: $ref: "#/definitions/error" - security: [ ] + security: [] tags: - UserAPI @@ -76,7 +76,7 @@ paths: description: Generic error response. schema: $ref: "#/definitions/error" - security: [ ] + security: [] tags: - UserAPI @@ -110,7 +110,6 @@ paths: tags: - UserAPI - /subscription/info: get: summary: Subscription info @@ -188,7 +187,6 @@ paths: tags: - OperatorAPI - /tenants: get: summary: List Tenant of All Namespaces @@ -794,6 +792,29 @@ paths: tags: - OperatorAPI + /cluster/allocatable-resources: + get: + summary: Get allocatable cpu and memory for given number of nodes + operationId: GetAllocatableResources + parameters: + - name: num_nodes + in: query + required: true + type: integer + format: int32 + minimum: 1 + responses: + 200: + description: A successful response. + schema: + $ref: "#/definitions/allocatableResourcesResponse" + default: + description: Generic error response. + schema: + $ref: "#/definitions/error" + tags: + - OperatorAPI + /get-parity/{nodes}/{disksPerNode}: get: summary: Gets parity by sending number of nodes & number of disks @@ -926,7 +947,6 @@ paths: - OperatorAPI definitions: - error: type: object required: @@ -945,7 +965,7 @@ definitions: properties: loginStrategy: type: string - enum: [ form, redirect, service-account ] + enum: [form, redirect, service-account] redirect: type: string loginRequest: @@ -980,7 +1000,7 @@ definitions: properties: status: type: string - enum: [ ok ] + enum: [ok] operator: type: boolean permissions: @@ -1325,10 +1345,10 @@ definitions: type: string securityContext: type: object - $ref: '#/definitions/securityContext' + $ref: "#/definitions/securityContext" postgres_securityContext: type: object - $ref: '#/definitions/securityContext' + $ref: "#/definitions/securityContext" postgres_image: type: string postgres_init_image: @@ -1350,7 +1370,7 @@ definitions: type: string securityContext: type: object - $ref: '#/definitions/securityContext' + $ref: "#/definitions/securityContext" idpConfiguration: type: object properties: @@ -1454,7 +1474,7 @@ definitions: $ref: "#/definitions/azureConfiguration" securityContext: type: object - $ref: '#/definitions/securityContext' + $ref: "#/definitions/securityContext" vaultConfiguration: type: object @@ -1701,7 +1721,7 @@ definitions: $ref: "#/definitions/poolTolerations" securityContext: type: object - $ref: '#/definitions/securityContext' + $ref: "#/definitions/securityContext" poolTolerations: description: Tolerations allows users to set entries like effect, @@ -2150,7 +2170,6 @@ definitions: items: type: string - subscriptionValidateRequest: type: object properties: @@ -2226,7 +2245,6 @@ definitions: drive: type: string - csiFormatErrorResponse: type: object properties: @@ -2330,19 +2348,42 @@ definitions: force: type: boolean securityContext: - type: object - required: - - runAsUser - - runAsGroup - - runAsNonRoot - - fsGroup - properties: - runAsUser: - type: string - runAsGroup: - type: string - runAsNonRoot: - type: boolean - fsGroup: - type: string + type: object + required: + - runAsUser + - runAsGroup + - runAsNonRoot + - fsGroup + properties: + runAsUser: + type: string + runAsGroup: + type: string + runAsNonRoot: + type: boolean + fsGroup: + type: string + allocatableResourcesResponse: + type: object + properties: + min_allocatable_mem: + type: integer + format: int64 + min_allocatable_cpu: + type: integer + format: int64 + cpu_priority: + $ref: "#/definitions/nodeMaxAllocatableResources" + mem_priority: + $ref: "#/definitions/nodeMaxAllocatableResources" + + nodeMaxAllocatableResources: + type: object + properties: + max_allocatable_cpu: + type: integer + format: int64 + max_allocatable_mem: + type: integer + format: int64