@@ -3,11 +3,12 @@ package cmd
33import (
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
1920Examples:
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}
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
49107func 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 }
0 commit comments