Skip to content

Commit 0713b56

Browse files
nathanjcochransyvb
andauthored
Support resizing services (#134)
Signed-off-by: Smitty <me@iter.ca> Co-authored-by: syvb <smitty@timescale.com> Co-authored-by: Smitty <me@iter.ca>
1 parent ee66208 commit 0713b56

File tree

16 files changed

+811
-176
lines changed

16 files changed

+811
-176
lines changed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ Tiger CLI is a Go-based command-line interface for managing Tiger, the modern da
267267
- **Command Structure**: `internal/tiger/cmd/` - Cobra-based command definitions
268268
- `root.go` - Root command with global flags and configuration initialization
269269
- `auth.go` - Authentication commands (login, logout, status)
270-
- `service.go` - Service management commands (list, create, get, fork, start, stop, delete, update-password)
270+
- `service.go` - Service management commands (list, create, get, fork, start, stop, resize, delete, update-password)
271271
- `db.go` - Database operation commands (connection-string, connect, test-connection)
272272
- `config.go` - Configuration management commands (show, set, unset, reset)
273273
- `mcp.go` - MCP server commands (install, start, list, get)
@@ -277,7 +277,7 @@ Tiger CLI is a Go-based command-line interface for managing Tiger, the modern da
277277
- **API Client**: `internal/tiger/api/` - Generated OpenAPI client with mocks
278278
- **MCP Server**: `internal/tiger/mcp/` - Model Context Protocol server implementation
279279
- `server.go` - MCP server initialization, tool registration, and lifecycle management
280-
- `service_tools.go` - Service management tools (list, get, create, fork, start, stop, update-password)
280+
- `service_tools.go` - Service management tools (list, get, create, fork, start, stop, resize, update-password)
281281
- `db_tools.go` - Database operation tools (execute-query)
282282
- `proxy.go` - Proxy client that forwards tools/resources/prompts from remote docs MCP server
283283
- `capabilities.go` - Lists all available MCP capabilities (tools, prompts, resources, resource templates)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Tiger CLI provides the following commands:
9696
- `fork` - Fork an existing service
9797
- `start` - Start a stopped service
9898
- `stop` - Stop a running service
99+
- `resize` - Resize service CPU and memory allocation
99100
- `delete` - Delete a service
100101
- `update-password` - Update service master password
101102
- `tiger db` - Database operations
@@ -179,6 +180,7 @@ The MCP server exposes the following tools to AI assistants:
179180
- `service_fork` - Fork an existing database service to create an independent copy
180181
- `service_start` - Start a stopped database service
181182
- `service_stop` - Stop a running database service
183+
- `service_resize` - Resize a database service by changing CPU and memory allocation
182184
- `service_update_password` - Update the master password for a service
183185

184186
**Database Operations:**

internal/tiger/api/client.go

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

internal/tiger/api/types.go

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

internal/tiger/cmd/db.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/base64"
77
"errors"
88
"fmt"
9+
"net/http"
910
"os"
1011
"os/exec"
1112
"strings"
@@ -787,7 +788,7 @@ func getServiceDetails(cmd *cobra.Command, cfg *common.Config, args []string) (a
787788
}
788789

789790
// Handle API response
790-
if resp.StatusCode() != 200 {
791+
if resp.StatusCode() != http.StatusOK {
791792
return api.Service{}, common.ExitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
792793
}
793794

internal/tiger/cmd/integration_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,146 @@ func TestServiceLifecycleIntegration(t *testing.T) {
10161016
}
10171017
})
10181018

1019+
t.Run("ResizeService", func(t *testing.T) {
1020+
if serviceID == "" {
1021+
t.Skip("No service ID available from create test")
1022+
}
1023+
1024+
t.Logf("Resizing service: %s", serviceID)
1025+
1026+
// First, get current service details to see current CPU/memory
1027+
output, err := executeIntegrationCommand(
1028+
t.Context(),
1029+
"service", "describe", serviceID,
1030+
"--output", "json",
1031+
)
1032+
1033+
if err != nil {
1034+
t.Fatalf("Failed to describe service before resize: %v\nOutput: %s", err, output)
1035+
}
1036+
1037+
// Parse JSON to check current resources
1038+
var serviceBefore api.Service
1039+
if err := json.Unmarshal([]byte(output), &serviceBefore); err != nil {
1040+
t.Fatalf("Failed to parse service JSON: %v", err)
1041+
}
1042+
1043+
var currentCPU, currentMemory string
1044+
if serviceBefore.Resources != nil && len(*serviceBefore.Resources) > 0 {
1045+
resource := (*serviceBefore.Resources)[0]
1046+
if resource.Spec != nil {
1047+
if resource.Spec.CpuMillis != nil {
1048+
cpuCores := float64(*resource.Spec.CpuMillis) / 1000
1049+
currentCPU = fmt.Sprintf("%.1f CPU", cpuCores)
1050+
}
1051+
if resource.Spec.MemoryGbs != nil {
1052+
currentMemory = fmt.Sprintf("%d GB", *resource.Spec.MemoryGbs)
1053+
}
1054+
}
1055+
}
1056+
1057+
t.Logf("Current resources: CPU=%s, Memory=%s", currentCPU, currentMemory)
1058+
1059+
// Resize to 1 CPU / 4 GB (larger than default 0.5 CPU / 2 GB)
1060+
// Note: --cpu expects millicores (1000 = 1 CPU), --memory expects GB as integer
1061+
targetCPUMillis := "1000" // 1 CPU = 1000 millicores
1062+
targetMemoryGB := "4" // 4 GB
1063+
1064+
t.Logf("Resizing to: CPU=%s millicores (1 CPU), Memory=%s GB", targetCPUMillis, targetMemoryGB)
1065+
1066+
output, err = executeIntegrationCommand(
1067+
t.Context(),
1068+
"service", "resize", serviceID,
1069+
"--cpu", targetCPUMillis,
1070+
"--memory", targetMemoryGB,
1071+
"--wait-timeout", "10m", // Longer timeout for resize operations
1072+
)
1073+
1074+
if err != nil {
1075+
t.Fatalf("Service resize failed: %v\nOutput: %s", err, output)
1076+
}
1077+
1078+
// Verify resize success message
1079+
if !strings.Contains(output, "Resize completed successfully") &&
1080+
!strings.Contains(output, "resized successfully") {
1081+
t.Logf("Note: Expected resize success message, got: %s", output)
1082+
}
1083+
1084+
t.Logf("Service resize command completed successfully")
1085+
})
1086+
1087+
t.Run("VerifyServiceResized", func(t *testing.T) {
1088+
if serviceID == "" {
1089+
t.Skip("No service ID available from create test")
1090+
}
1091+
1092+
t.Logf("Verifying service has been resized")
1093+
1094+
output, err := executeIntegrationCommand(
1095+
t.Context(),
1096+
"service", "describe", serviceID,
1097+
"--output", "json",
1098+
)
1099+
1100+
if err != nil {
1101+
t.Fatalf("Failed to describe service after resize: %v\nOutput: %s", err, output)
1102+
}
1103+
1104+
// Parse JSON to check new resources
1105+
var serviceAfter api.Service
1106+
if err := json.Unmarshal([]byte(output), &serviceAfter); err != nil {
1107+
t.Fatalf("Failed to parse service JSON: %v", err)
1108+
}
1109+
1110+
var newCPUMillis, newMemoryGbs int
1111+
if serviceAfter.Resources != nil && len(*serviceAfter.Resources) > 0 {
1112+
resource := (*serviceAfter.Resources)[0]
1113+
if resource.Spec != nil {
1114+
if resource.Spec.CpuMillis != nil {
1115+
newCPUMillis = *resource.Spec.CpuMillis
1116+
}
1117+
if resource.Spec.MemoryGbs != nil {
1118+
newMemoryGbs = *resource.Spec.MemoryGbs
1119+
}
1120+
}
1121+
}
1122+
1123+
newCPU := fmt.Sprintf("%.1f CPU", float64(newCPUMillis)/1000)
1124+
newMemory := fmt.Sprintf("%d GB", newMemoryGbs)
1125+
1126+
t.Logf("New resources after resize: CPU=%s (millis=%d), Memory=%s", newCPU, newCPUMillis, newMemory)
1127+
1128+
// Verify the service has been resized to expected values
1129+
expectedCPUMillis := 1000 // 1 CPU = 1000 millicores
1130+
expectedMemoryGbs := 4 // 4 GB
1131+
1132+
if newCPUMillis != expectedCPUMillis {
1133+
t.Errorf("Expected CPU to be %d millicores after resize, got %d", expectedCPUMillis, newCPUMillis)
1134+
} else {
1135+
t.Logf("✅ CPU correctly resized to %d millicores (1 CPU)", newCPUMillis)
1136+
}
1137+
1138+
if newMemoryGbs != expectedMemoryGbs {
1139+
t.Errorf("Expected Memory to be %d GB after resize, got %d", expectedMemoryGbs, newMemoryGbs)
1140+
} else {
1141+
t.Logf("✅ Memory correctly resized to %d GB", newMemoryGbs)
1142+
}
1143+
1144+
// Verify service is still in READY state after resize
1145+
var status string
1146+
if serviceAfter.Status != nil {
1147+
status = string(*serviceAfter.Status)
1148+
}
1149+
1150+
if status != "READY" {
1151+
t.Logf("Warning: Expected service status to be READY after resize, got %s", status)
1152+
} else {
1153+
t.Logf("✅ Service is correctly in READY state after resize")
1154+
}
1155+
1156+
t.Logf("✅ Service resize verified successfully")
1157+
})
1158+
10191159
t.Run("DeleteService", func(t *testing.T) {
10201160
if serviceID == "" {
10211161
t.Skip("No service ID available for deletion")
@@ -1572,6 +1712,11 @@ func TestServiceForkIntegration(t *testing.T) {
15721712
t.Fatalf("Login failed: %v\nOutput: %s", err, output)
15731713
}
15741714

1715+
// Verify login success message
1716+
if !strings.Contains(output, "Successfully logged in") && !strings.Contains(output, "Logged in") {
1717+
t.Errorf("Login output: %s", output)
1718+
}
1719+
15751720
t.Logf("Login successful")
15761721
})
15771722

0 commit comments

Comments
 (0)