Skip to content
Open
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
121 changes: 105 additions & 16 deletions pkg/toolsets/core/create_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"context"
"fmt"
"strconv"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/rancher/rancher-ai-mcp/internal/middleware"
Expand All @@ -13,15 +14,38 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type resourceQuota struct {
Pods int `json:"pods,omitempty" jsonschema:"the maximum number of pods that can be created in the project"`
Services int `json:"services,omitempty" jsonschema:"the maximum number of services that can be created in the project"`
ReplicationControllers int `json:"replicationControllers,omitempty" jsonschema:"the maximum number of replication controllers that can be created in the project"`
Secrets int `json:"secrets,omitempty" jsonschema:"the maximum number of secrets that can be created in the project"`
ConfigMaps int `json:"configMaps,omitempty" jsonschema:"the maximum number of config maps that can be created in the project"`
PersistentVolumeClaims int `json:"persistentVolumeClaims,omitempty" jsonschema:"the maximum number of persistent volume claims that can be created in the project"`
ServicesNodePorts int `json:"servicesNodePorts,omitempty" jsonschema:"the maximum number of services with node ports that can be created in the project"`
ServicesLoadBalancers int `json:"servicesLoadBalancers,omitempty" jsonschema:"the maximum number of services with load balancers that can be created in the project"`
RequestsCPU int `json:"requestsCpu,omitempty" jsonschema:"the amount of CPU resources (mCPUs) reserved for containers in the project"`
RequestsMemory int `json:"requestsMemory,omitempty" jsonschema:"the amount of memory resources (MiB) reserved for containers in the project"`
RequestsStorage int `json:"requestsStorage,omitempty" jsonschema:"the amount of storage resources (MiB) reserved for containers in the project"`
LimitsCPU int `json:"limitsCpu,omitempty" jsonschema:"the maximum amount of CPU resources (mCPUs) that can be used by containers in the project"`
LimitsMemory int `json:"limitsMemory,omitempty" jsonschema:"the maximum amount of memory resources (MiB) that can be used by containers in the project"`
Extended map[string]any `json:"extended,omitempty" jsonschema:"a map of any additional resource quotas to be applied to the project, where the key is the name of the resource quota and the value is the quantity (e.g., '10Gi' for storage)"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this field used for? LLM tries to add a string in the Extended field and returns an error.

}

type containerDefaultResourceLimit struct {
CPULimit int `json:"cpuLimit,omitempty" jsonschema:"the maximum amount of CPU resources (mCPUs) that can be used by containers in the project"`
CPUReservation int `json:"cpuReservation,omitempty" jsonschema:"the amount of CPU resources (mCPUs) reserved for containers in the project"`
MemoryLimit int `json:"memoryLimit,omitempty" jsonschema:"the maximum amount of memory resources (MiB) that can be used by containers in the project"`
MemoryReservation int `json:"memoryReservation,omitempty" jsonschema:"the amount of memory resources (MiB) reserved for containers in the project"`
}

type createProjectParams struct {
Cluster string `json:"cluster" jsonschema:"the cluster that the project belongs to"`
Name string `json:"name" jsonschema:"the name of the project to be created"`
Description string `json:"description,omitempty" jsonschema:"an optional description for the project"`
DisplayName string `json:"displayName,omitempty" jsonschema:"an optional display name for the project"`
CPULimit int `json:"cpuLimit,omitempty" jsonschema:"the maximum amount of CPU resources (mCPUs) that can be used by containers in the project"`
CPUReservation int `json:"cpuReservation,omitempty" jsonschema:"the amount of CPU resources (mCPUs) reserved for containers in the project"`
MemoryLimit int `json:"memoryLimit,omitempty" jsonschema:"the maximum amount of memory resources (MiB) that can be used by containers in the project"`
MemoryReservation int `json:"memoryReservation,omitempty" jsonschema:"the amount of memory resources (MiB) reserved for containers in the project"`
Cluster string `json:"cluster" jsonschema:"the cluster that the project belongs to"`
Name string `json:"name" jsonschema:"the name of the project to be created"`
Description string `json:"description,omitempty" jsonschema:"an optional description for the project"`
DisplayName string `json:"displayName,omitempty" jsonschema:"an optional display name for the project"`
ResourceQuota resourceQuota `json:"resourceQuota,omitempty" jsonschema:"optional resource quotas to be applied to the project"`
NamespaceDefaultResourceQuota resourceQuota `json:"namespaceDefaultResourceQuota,omitempty" jsonschema:"optional default resource quotas to be applied to namespaces created within the project"`
ContainerDefaultResourceLimit containerDefaultResourceLimit `json:"containerDefaultResourceLimit,omitempty" jsonschema:"optional default resource limits to be applied to containers created within the project"`
}

func (t *Tools) createProject(ctx context.Context, toolReq *mcp.CallToolRequest, params createProjectParams) (*mcp.CallToolResult, any, error) {
Expand Down Expand Up @@ -85,22 +109,87 @@ func (t *Tools) createProjectObj(params createProjectParams) (*unstructured.Unst

// Create any container resource quotas if specified with their respective units
containerResourceQuotas := make(map[string]any)
if params.CPULimit != 0 {
containerResourceQuotas["limitsCpu"] = fmt.Sprintf("%dm", params.CPULimit)
if params.ContainerDefaultResourceLimit.CPULimit != 0 {
containerResourceQuotas["limitsCpu"] = fmt.Sprintf("%dm", params.ContainerDefaultResourceLimit.CPULimit)
}
if params.CPUReservation != 0 {
containerResourceQuotas["requestsCpu"] = fmt.Sprintf("%dm", params.CPUReservation)
if params.ContainerDefaultResourceLimit.CPUReservation != 0 {
containerResourceQuotas["requestsCpu"] = fmt.Sprintf("%dm", params.ContainerDefaultResourceLimit.CPUReservation)
}
if params.MemoryLimit != 0 {
containerResourceQuotas["limitsMemory"] = fmt.Sprintf("%dMi", params.MemoryLimit)
if params.ContainerDefaultResourceLimit.MemoryLimit != 0 {
containerResourceQuotas["limitsMemory"] = fmt.Sprintf("%dMi", params.ContainerDefaultResourceLimit.MemoryLimit)
}
if params.MemoryReservation != 0 {
containerResourceQuotas["requestsMemory"] = fmt.Sprintf("%dMi", params.MemoryReservation)
if params.ContainerDefaultResourceLimit.MemoryReservation != 0 {
containerResourceQuotas["requestsMemory"] = fmt.Sprintf("%dMi", params.ContainerDefaultResourceLimit.MemoryReservation)
}

if err := unstructured.SetNestedField(project.Object, containerResourceQuotas, "spec", "containerDefaultResourceLimit"); err != nil {
return nil, fmt.Errorf("failed to set project container resource quotas: %w", err)
}

// Create the resource quota map if any quotas were specified
quotaMap := buildResourceQuotaMap(params.ResourceQuota)
if quotaMap["limit"] != nil {
if err := unstructured.SetNestedField(project.Object, quotaMap, "spec", "resourceQuota"); err != nil {
return nil, fmt.Errorf("failed to set project resource quotas: %w", err)
}
}

// Create the namespaces default resource quota map if any quotas were specified
namespaceResourceQuota := buildResourceQuotaMap(params.NamespaceDefaultResourceQuota)
if namespaceResourceQuota["limit"] != nil {
if err := unstructured.SetNestedField(project.Object, namespaceResourceQuota, "spec", "namespaceDefaultResourceQuota"); err != nil {
return nil, fmt.Errorf("failed to set project namespace default resource quotas: %w", err)
}
}

return project, nil
}

func buildResourceQuotaMap(quota resourceQuota) map[string]any {
limitMap := make(map[string]any)
if quota.Pods != 0 {
limitMap["pods"] = strconv.Itoa(quota.Pods)
}
if quota.Services != 0 {
limitMap["services"] = strconv.Itoa(quota.Services)
}
if quota.ReplicationControllers != 0 {
limitMap["replicationControllers"] = strconv.Itoa(quota.ReplicationControllers)
}
if quota.Secrets != 0 {
limitMap["secrets"] = strconv.Itoa(quota.Secrets)
}
if quota.ConfigMaps != 0 {
limitMap["configMaps"] = strconv.Itoa(quota.ConfigMaps)
}
if quota.PersistentVolumeClaims != 0 {
limitMap["persistentVolumeClaims"] = strconv.Itoa(quota.PersistentVolumeClaims)
}
if quota.ServicesNodePorts != 0 {
limitMap["servicesNodePorts"] = strconv.Itoa(quota.ServicesNodePorts)
}
if quota.ServicesLoadBalancers != 0 {
limitMap["servicesLoadBalancers"] = strconv.Itoa(quota.ServicesLoadBalancers)
}
if quota.RequestsCPU != 0 {
limitMap["requestsCpu"] = fmt.Sprintf("%dm", quota.RequestsCPU)
}
if quota.RequestsMemory != 0 {
limitMap["requestsMemory"] = fmt.Sprintf("%dMi", quota.RequestsMemory)
}
if quota.RequestsStorage != 0 {
limitMap["requestsStorage"] = fmt.Sprintf("%dMi", quota.RequestsStorage)
}
if quota.LimitsCPU != 0 {
limitMap["limitsCpu"] = fmt.Sprintf("%dm", quota.LimitsCPU)
}
if quota.LimitsMemory != 0 {
limitMap["limitsMemory"] = fmt.Sprintf("%dMi", quota.LimitsMemory)
}
if quota.Extended != nil {
limitMap["extended"] = quota.Extended
}
quotaMap := make(map[string]any)
quotaMap["limit"] = limitMap
return quotaMap
}
155 changes: 148 additions & 7 deletions pkg/toolsets/core/create_project_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ func TestCreateProjectPlan(t *testing.T) {
},
"create project plan with resource limits": {
params: createProjectParams{
Cluster: "local",
Name: "project-with-limits",
DisplayName: "Project with Limits",
CPULimit: 2000,
CPUReservation: 1000,
MemoryLimit: 4096,
MemoryReservation: 2048,
Cluster: "local",
Name: "project-with-limits",
DisplayName: "Project with Limits",
ContainerDefaultResourceLimit: containerDefaultResourceLimit{
CPULimit: 2000,
CPUReservation: 1000,
MemoryLimit: 4096,
MemoryReservation: 2048,
},
},
validateResult: func(t *testing.T, result string) {
var planResources []response.PlanResource
Expand Down Expand Up @@ -118,6 +120,145 @@ func TestCreateProjectPlan(t *testing.T) {
assert.True(t, ok)
},
},
"create project plan with namespace default resource quotas": {
params: createProjectParams{
Cluster: "local",
Name: "project-with-ns-quotas",
DisplayName: "Project with Namespace Quotas",
NamespaceDefaultResourceQuota: resourceQuota{
Pods: 100,
Services: 50,
Secrets: 100,
ConfigMaps: 50,
PersistentVolumeClaims: 10,
RequestsCPU: 5000,
RequestsMemory: 10240,
},
},
validateResult: func(t *testing.T, result string) {
var planResources []response.PlanResource
err := json.Unmarshal([]byte(result), &planResources)
require.NoError(t, err)
require.Len(t, planResources, 1)

planResource := planResources[0]
payload, ok := planResource.Payload.(map[string]any)
require.True(t, ok)

spec, ok := payload["spec"].(map[string]any)
require.True(t, ok)

nsQuota, ok := spec["namespaceDefaultResourceQuota"].(map[string]any)
require.True(t, ok)
limit, ok := nsQuota["limit"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "100", limit["pods"])
assert.Equal(t, "50", limit["services"])
assert.Equal(t, "100", limit["secrets"])
assert.Equal(t, "50", limit["configMaps"])
assert.Equal(t, "10", limit["persistentVolumeClaims"])
assert.Equal(t, "5000m", limit["requestsCpu"])
assert.Equal(t, "10240Mi", limit["requestsMemory"])
},
},
"create project plan with resource quotas": {
params: createProjectParams{
Cluster: "local",
Name: "project-with-quotas",
DisplayName: "Project with Resource Quotas",
ResourceQuota: resourceQuota{
ReplicationControllers: 20,
ServicesNodePorts: 5,
ServicesLoadBalancers: 3,
LimitsCPU: 10000,
LimitsMemory: 20480,
RequestsStorage: 51200,
},
},
validateResult: func(t *testing.T, result string) {
var planResources []response.PlanResource
err := json.Unmarshal([]byte(result), &planResources)
require.NoError(t, err)
require.Len(t, planResources, 1)

planResource := planResources[0]
payload, ok := planResource.Payload.(map[string]any)
require.True(t, ok)

spec, ok := payload["spec"].(map[string]any)
require.True(t, ok)

resQuota, ok := spec["resourceQuota"].(map[string]any)
require.True(t, ok)
limit, ok := resQuota["limit"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "20", limit["replicationControllers"])
assert.Equal(t, "5", limit["servicesNodePorts"])
assert.Equal(t, "3", limit["servicesLoadBalancers"])
assert.Equal(t, "10000m", limit["limitsCpu"])
assert.Equal(t, "20480Mi", limit["limitsMemory"])
assert.Equal(t, "51200Mi", limit["requestsStorage"])
},
},
"create project plan with all fields": {
params: createProjectParams{
Cluster: "local",
Name: "comprehensive-project",
DisplayName: "Comprehensive Project",
Description: "A project with all fields set",
ContainerDefaultResourceLimit: containerDefaultResourceLimit{
CPULimit: 1500,
CPUReservation: 500,
MemoryLimit: 2048,
MemoryReservation: 1024,
},
NamespaceDefaultResourceQuota: resourceQuota{
Pods: 50,
Services: 25,
},
ResourceQuota: resourceQuota{
ServicesNodePorts: 10,
LimitsCPU: 8000,
},
},
validateResult: func(t *testing.T, result string) {
var planResources []response.PlanResource
err := json.Unmarshal([]byte(result), &planResources)
require.NoError(t, err)
require.Len(t, planResources, 1)

planResource := planResources[0]
payload, ok := planResource.Payload.(map[string]any)
require.True(t, ok)

spec, ok := payload["spec"].(map[string]any)
require.True(t, ok)

// Validate container default resource limits
containerLimits, ok := spec["containerDefaultResourceLimit"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "1500m", containerLimits["limitsCpu"])
assert.Equal(t, "500m", containerLimits["requestsCpu"])
assert.Equal(t, "2048Mi", containerLimits["limitsMemory"])
assert.Equal(t, "1024Mi", containerLimits["requestsMemory"])

// Validate namespace default resource quotas
nsQuota, ok := spec["namespaceDefaultResourceQuota"].(map[string]any)
require.True(t, ok)
nsLimit, ok := nsQuota["limit"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "50", nsLimit["pods"])
assert.Equal(t, "25", nsLimit["services"])

// Validate resource quotas
resQuota, ok := spec["resourceQuota"].(map[string]any)
require.True(t, ok)
resLimit, ok := resQuota["limit"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "10", resLimit["servicesNodePorts"])
assert.Equal(t, "8000m", resLimit["limitsCpu"])
},
},
}

for name, tt := range tests {
Expand Down
Loading