Skip to content

Commit 61da18c

Browse files
committed
Added tcp-port flag and UI Improves
1 parent 20ad2df commit 61da18c

File tree

6 files changed

+243
-32
lines changed

6 files changed

+243
-32
lines changed

cmd/deploy.go

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package cmd
33
import (
44
"fmt"
55
"os"
6-
7-
"github.com/spf13/cobra"
6+
"regexp"
7+
"strings"
88

99
"github.com/helmcode/coderun-cli/internal/client"
1010
"github.com/helmcode/coderun-cli/internal/utils"
11+
"github.com/spf13/cobra"
1112
)
1213

1314
// deployCmd represents the deploy command
@@ -17,10 +18,12 @@ var deployCmd = &cobra.Command{
1718
Long: `Deploy a Docker container to the CodeRun platform.
1819
1920
Examples:
20-
coderun deploy nginx:latest
21-
coderun deploy my-app:v1.0 --replicas=3 --cpu=500m --memory=1Gi
22-
coderun deploy my-app:latest --http-port=8080 --env-file=.env
23-
coderun deploy my-app:latest --replicas=2 --cpu=200m --memory=512Mi --http-port=3000 --env-file=production.env`,
21+
coderun deploy nginx:latest --name my-nginx
22+
coderun deploy my-app:v1.0 --name my-app --replicas 3 --cpu 500m --memory 1Gi
23+
coderun deploy my-app:latest --name web-app --http-port 8080 --env-file .env
24+
coderun deploy redis:latest --name my-redis --tcp-port 6379
25+
coderun deploy postgres:latest --name my-db --tcp-port 5432 --env-file database.env
26+
coderun deploy my-app:latest --name prod-app --replicas 2 --cpu 200m --memory 512Mi --http-port 3000 --env-file production.env`,
2427
Args: cobra.ExactArgs(1),
2528
Run: runDeploy,
2629
}
@@ -30,6 +33,7 @@ var (
3033
cpu string
3134
memory string
3235
httpPort int
36+
tcpPort int
3337
envFile string
3438
appName string
3539
)
@@ -42,8 +46,62 @@ func init() {
4246
deployCmd.Flags().StringVar(&cpu, "cpu", "", "CPU resource limit (e.g., 100m, 0.5)")
4347
deployCmd.Flags().StringVar(&memory, "memory", "", "Memory resource limit (e.g., 128Mi, 1Gi)")
4448
deployCmd.Flags().IntVar(&httpPort, "http-port", 0, "HTTP port to expose")
49+
deployCmd.Flags().IntVar(&tcpPort, "tcp-port", 0, "TCP port to expose")
4550
deployCmd.Flags().StringVar(&envFile, "env-file", "", "Path to environment file")
46-
deployCmd.Flags().StringVar(&appName, "name", "", "Application name (optional, auto-generated if not provided)")
51+
deployCmd.Flags().StringVar(&appName, "name", "", "Application name (required, 3-30 chars, lowercase letters/numbers/hyphens only)")
52+
}
53+
54+
// parseValidationError tries to parse backend validation errors and return user-friendly messages
55+
func parseValidationError(errorMsg string) string {
56+
// Convert to lowercase for easier matching
57+
lowerError := strings.ToLower(errorMsg)
58+
59+
// App name validation errors
60+
if strings.Contains(lowerError, "app_name") {
61+
if strings.Contains(lowerError, "at least 3 characters") {
62+
return "App name must be at least 3 characters long. Use --name to specify one (e.g., --name my-app)"
63+
}
64+
if strings.Contains(lowerError, "at most 30 characters") || strings.Contains(lowerError, "no more than 30") {
65+
return "App name must be no more than 30 characters long"
66+
}
67+
if strings.Contains(lowerError, "lowercase") || strings.Contains(lowerError, "letters") || strings.Contains(lowerError, "hyphens") {
68+
return "App name must contain only lowercase letters, numbers, and hyphens"
69+
}
70+
return "Invalid app name. Use --name to specify one (3-30 chars, lowercase letters/numbers/hyphens only)"
71+
}
72+
73+
// Port validation errors
74+
if strings.Contains(lowerError, "both http_port and tcp_port") || strings.Contains(lowerError, "both ports") {
75+
return "Cannot specify both --http-port and --tcp-port. Choose one type of port"
76+
}
77+
if strings.Contains(lowerError, "http_port") || strings.Contains(lowerError, "tcp_port") || strings.Contains(lowerError, "port") {
78+
return "Port must be a valid number between 1 and 65535"
79+
}
80+
81+
// Resource validation errors
82+
if strings.Contains(lowerError, "cpu") && (strings.Contains(lowerError, "invalid") || strings.Contains(lowerError, "format")) {
83+
return "Invalid CPU value. Use format like '100m' or '0.5'"
84+
}
85+
if strings.Contains(lowerError, "memory") && (strings.Contains(lowerError, "invalid") || strings.Contains(lowerError, "format")) {
86+
return "Invalid memory value. Use format like '128Mi' or '1Gi'"
87+
}
88+
89+
// Image validation errors
90+
if strings.Contains(lowerError, "image") && strings.Contains(lowerError, "at least 1") {
91+
return "Image name cannot be empty"
92+
}
93+
94+
// Generic validation error
95+
if strings.Contains(lowerError, "422") || strings.Contains(lowerError, "validation") {
96+
return "Validation error: Please check your input parameters"
97+
}
98+
99+
// If we can't parse it, return a cleaner version of the original error
100+
if strings.Contains(errorMsg, "HTTP 422:") {
101+
return "Validation error: Please check your input parameters and try again"
102+
}
103+
104+
return errorMsg
47105
}
48106

49107
func runDeploy(cmd *cobra.Command, args []string) {
@@ -72,6 +130,49 @@ func runDeploy(cmd *cobra.Command, args []string) {
72130
os.Exit(1)
73131
}
74132

133+
// Validate that only one of HTTP or TCP port is specified
134+
if httpPort > 0 && tcpPort > 0 {
135+
fmt.Println("Cannot specify both --http-port and --tcp-port")
136+
os.Exit(1)
137+
}
138+
139+
// Validate app name if provided
140+
if appName != "" {
141+
if len(appName) < 3 {
142+
fmt.Println("App name must be at least 3 characters long")
143+
os.Exit(1)
144+
}
145+
if len(appName) > 30 {
146+
fmt.Println("App name must be no more than 30 characters long")
147+
os.Exit(1)
148+
}
149+
// Validate format using regex: only lowercase letters, numbers, and hyphens
150+
matched, _ := regexp.MatchString(`^[a-z0-9-]+$`, appName)
151+
if !matched {
152+
fmt.Println("App name must contain only lowercase letters, numbers, and hyphens")
153+
os.Exit(1)
154+
}
155+
// Cannot start or end with hyphen
156+
if strings.HasPrefix(appName, "-") || strings.HasSuffix(appName, "-") {
157+
fmt.Println("App name cannot start or end with a hyphen")
158+
os.Exit(1)
159+
}
160+
} else {
161+
fmt.Println("App name is required. Use --name to specify one (e.g., --name my-app)")
162+
fmt.Println("App name must be 3-30 characters long and contain only lowercase letters, numbers, and hyphens")
163+
os.Exit(1)
164+
}
165+
166+
// Validate port ranges
167+
if httpPort > 0 && (httpPort < 1 || httpPort > 65535) {
168+
fmt.Println("HTTP port must be between 1 and 65535")
169+
os.Exit(1)
170+
}
171+
if tcpPort > 0 && (tcpPort < 1 || tcpPort > 65535) {
172+
fmt.Println("TCP port must be between 1 and 65535")
173+
os.Exit(1)
174+
}
175+
75176
// Parse environment file if provided
76177
var envVars map[string]string
77178
if envFile != "" {
@@ -98,6 +199,11 @@ func runDeploy(cmd *cobra.Command, args []string) {
98199
deployReq.HTTPPort = &httpPort
99200
}
100201

202+
// Add TCP port if specified
203+
if tcpPort > 0 {
204+
deployReq.TCPPort = &tcpPort
205+
}
206+
101207
// Create client and deploy
102208
apiClient := client.NewClient(config.BaseURL)
103209
apiClient.SetToken(config.AccessToken)
@@ -106,9 +212,13 @@ func runDeploy(cmd *cobra.Command, args []string) {
106212
if httpPort > 0 {
107213
fmt.Println("ℹ️ Note: Deploy with HTTP port may take several minutes (waiting for TLS certificate)")
108214
}
215+
if tcpPort > 0 {
216+
fmt.Println("ℹ️ Note: Deploy with TCP port will be available in the NodePort range (30000-32767)")
217+
}
109218
deployment, err := apiClient.CreateDeployment(&deployReq)
110219
if err != nil {
111-
fmt.Printf("Deployment failed: %v\n", err)
220+
userFriendlyError := parseValidationError(err.Error())
221+
fmt.Printf("Deployment failed: %s\n", userFriendlyError)
112222
os.Exit(1)
113223
}
114224

@@ -127,6 +237,15 @@ func runDeploy(cmd *cobra.Command, args []string) {
127237
if deployment.HTTPPort != nil {
128238
fmt.Printf("HTTP Port: %d\n", *deployment.HTTPPort)
129239
}
240+
if deployment.TCPPort != nil {
241+
fmt.Printf("TCP Port: %d\n", *deployment.TCPPort)
242+
}
243+
if deployment.TCPNodePort != nil {
244+
fmt.Printf("TCP NodePort: %d\n", *deployment.TCPNodePort)
245+
}
246+
if deployment.TCPConnection != nil {
247+
fmt.Printf("TCP Connection: %s\n", *deployment.TCPConnection)
248+
}
130249
if len(deployment.EnvironmentVars) > 0 {
131250
fmt.Printf("Environment Variables: %d\n", len(deployment.EnvironmentVars))
132251
}

cmd/list.go

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"github.com/spf13/cobra"
89

@@ -25,6 +26,79 @@ func init() {
2526
rootCmd.AddCommand(listCmd)
2627
}
2728

29+
// calculateColumnWidths calculates the optimal width for each column based on content
30+
func calculateColumnWidths(deployments []client.DeploymentResponse) (int, int, int, int, int, int, int) {
31+
// Minimum widths for headers
32+
idWidth := len("ID")
33+
appNameWidth := len("App Name")
34+
imageWidth := len("Image")
35+
replicasWidth := len("Replicas")
36+
statusWidth := len("Status")
37+
connectionWidth := len("Connection")
38+
createdWidth := len("Created")
39+
40+
// Check content and adjust widths
41+
for _, deployment := range deployments {
42+
if len(deployment.ID) > idWidth {
43+
idWidth = len(deployment.ID)
44+
}
45+
if len(deployment.AppName) > appNameWidth {
46+
appNameWidth = len(deployment.AppName)
47+
}
48+
if len(deployment.Image) > imageWidth {
49+
imageWidth = len(deployment.Image)
50+
}
51+
52+
replicasStr := fmt.Sprintf("%d", deployment.Replicas)
53+
if len(replicasStr) > replicasWidth {
54+
replicasWidth = len(replicasStr)
55+
}
56+
57+
if len(deployment.Status) > statusWidth {
58+
statusWidth = len(deployment.Status)
59+
}
60+
61+
connection := getConnectionString(&deployment)
62+
if len(connection) > connectionWidth {
63+
connectionWidth = len(connection)
64+
}
65+
66+
createdStr := deployment.CreatedAt.Format("2006-01-02 15:04")
67+
if len(createdStr) > createdWidth {
68+
createdWidth = len(createdStr)
69+
}
70+
}
71+
72+
// Add some padding
73+
return idWidth + 2, appNameWidth + 2, imageWidth + 2, replicasWidth + 2, statusWidth + 2, connectionWidth + 2, createdWidth + 2
74+
}
75+
76+
// getConnectionString generates a user-friendly connection string based on the deployment type
77+
func getConnectionString(deployment *client.DeploymentResponse) string {
78+
// HTTP deployments - use the URL field from backend if available
79+
if deployment.URL != nil && *deployment.URL != "" {
80+
return *deployment.URL
81+
}
82+
83+
// Fallback for HTTP deployments if URL is not set yet
84+
if deployment.HTTPPort != nil {
85+
return fmt.Sprintf("https://%s.helmcode.com", deployment.AppName)
86+
}
87+
88+
// TCP deployments - prefer TCPConnection field if available
89+
if deployment.TCPConnection != nil && *deployment.TCPConnection != "" {
90+
return *deployment.TCPConnection
91+
}
92+
93+
// TCP with NodePort but no connection string yet
94+
if deployment.TCPPort != nil && deployment.TCPNodePort != nil {
95+
return fmt.Sprintf("67.207.79.206:%d", *deployment.TCPNodePort)
96+
}
97+
98+
// No exposed ports
99+
return "Internal only"
100+
}
101+
28102
func runList(cmd *cobra.Command, args []string) {
29103
// Load config
30104
config, err := utils.LoadConfig()
@@ -54,36 +128,44 @@ func runList(cmd *cobra.Command, args []string) {
54128
return
55129
}
56130

57-
// Display deployments with full IDs
58-
fmt.Printf("%-36s %-20s %-30s %-8s %-10s %-16s\n",
59-
"ID", "App Name", "Image", "Replicas", "Status", "Created")
60-
fmt.Printf("%-36s %-20s %-30s %-8s %-10s %-16s\n",
61-
"------------------------------------", "--------------------", "------------------------------",
62-
"--------", "----------", "----------------")
131+
// Calculate optimal column widths
132+
idWidth, appNameWidth, imageWidth, replicasWidth, statusWidth, connectionWidth, createdWidth := calculateColumnWidths(deploymentList.Deployments)
63133

64-
// Add rows with full IDs
65-
for _, deployment := range deploymentList.Deployments {
66-
// Truncate app name if too long
67-
appName := deployment.AppName
68-
if len(appName) > 20 {
69-
appName = appName[:18] + ".."
70-
}
134+
// Create format strings for headers and rows
135+
headerFormat := fmt.Sprintf("%%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds\n",
136+
idWidth, appNameWidth, imageWidth, replicasWidth, statusWidth, connectionWidth, createdWidth)
71137

72-
// Truncate image if too long
73-
image := deployment.Image
74-
if len(image) > 30 {
75-
image = image[:28] + ".."
76-
}
138+
separatorFormat := fmt.Sprintf("%%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds\n",
139+
idWidth, appNameWidth, imageWidth, replicasWidth, statusWidth, connectionWidth, createdWidth)
77140

141+
// Display headers
142+
fmt.Printf(headerFormat, "ID", "App Name", "Image", "Replicas", "Status", "Connection", "Created")
143+
144+
// Display separator
145+
fmt.Printf(separatorFormat,
146+
strings.Repeat("-", idWidth),
147+
strings.Repeat("-", appNameWidth),
148+
strings.Repeat("-", imageWidth),
149+
strings.Repeat("-", replicasWidth),
150+
strings.Repeat("-", statusWidth),
151+
strings.Repeat("-", connectionWidth),
152+
strings.Repeat("-", createdWidth))
153+
154+
// Display rows
155+
for _, deployment := range deploymentList.Deployments {
78156
// Format created date
79157
createdAt := deployment.CreatedAt.Format("2006-01-02 15:04")
80158

81-
fmt.Printf("%-36s %-20s %-30s %-8d %-10s %-16s\n",
159+
// Generate connection string (no truncation)
160+
connection := getConnectionString(&deployment)
161+
162+
fmt.Printf(headerFormat,
82163
deployment.ID,
83-
appName,
84-
image,
85-
deployment.Replicas,
164+
deployment.AppName,
165+
deployment.Image,
166+
fmt.Sprintf("%d", deployment.Replicas),
86167
deployment.Status,
168+
connection,
87169
createdAt)
88170
}
89171
}

cmd/logs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Shows the most recent logs from all pods in the deployment.
2424
2525
Examples:
2626
coderun logs abc12345-6789-def0-1234-567890abcdef
27-
coderun logs abc12345-6789-def0-1234-567890abcdef --lines=200`,
27+
coderun logs abc12345-6789-def0-1234-567890abcdef --lines 200`,
2828
Args: cobra.ExactArgs(1),
2929
Run: runLogs,
3030
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ in a Kubernetes cluster with ease. Deploy your applications with simple commands
1313
1414
Examples:
1515
coderun login # Authenticate with your account
16-
coderun deploy nginx:latest --replicas=2 # Deploy nginx with 2 replicas
16+
coderun deploy nginx:latest --replicas 2 # Deploy nginx with 2 replicas
1717
coderun list # List all your deployments
1818
coderun status <DEPLOYMENT_ID> # Check status by deployment ID`,
1919
}

cmd/status.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ func runStatus(cmd *cobra.Command, args []string) {
8080
}
8181
}
8282

83+
if status.TCPConnection != nil {
84+
fmt.Printf("TCP Connection: %s\n", *status.TCPConnection)
85+
}
86+
8387
if len(status.Pods) > 0 {
8488
fmt.Printf("\n� Pods:\n")
8589
for i, pod := range status.Pods {

0 commit comments

Comments
 (0)