55 "os"
66 "regexp"
77 "strings"
8+ "time"
89
910 "github.com/helmcode/coderun-cli/internal/client"
1011 "github.com/helmcode/coderun-cli/internal/utils"
@@ -13,24 +14,36 @@ import (
1314
1415// deployCmd represents the deploy command
1516var deployCmd = & cobra.Command {
16- Use : "deploy IMAGE" ,
17- Short : "Deploy a Docker container" ,
18- Long : `Deploy a Docker container to the CodeRun platform.
17+ Use : "deploy [ IMAGE] " ,
18+ Short : "Deploy a Docker container or build from source " ,
19+ Long : `Deploy a Docker container to the CodeRun platform or build from source .
1920
20- Examples :
21+ Deploy from existing image :
2122 coderun deploy nginx:latest --name my-nginx
2223 coderun deploy my-app:v1.0 --name my-app --replicas 3 --cpu 500m --memory 1Gi
2324 coderun deploy my-app:latest --name web-app --http-port 8080 --env-file .env
2425 coderun deploy redis:latest --name my-redis --tcp-port 6379
2526 coderun deploy postgres:latest --name my-db --tcp-port 5432 --env-file database.env
2627 coderun deploy my-app:latest --name prod-app --replicas 2 --cpu 200m --memory 512Mi --http-port 3000 --env-file production.env
28+
29+ Build from source:
30+ coderun deploy --build . --name my-app
31+ coderun deploy --build ./my-app --name my-app --dockerfile Dockerfile.prod
32+ coderun deploy --build . --name web-app --http-port 8080 --env-file .env
2733
28- # With persistent storage (automatically forces replicas to 1):
34+ With persistent storage (automatically forces replicas to 1):
2935 coderun deploy postgres:15 --name my-postgres --tcp-port 5432 --storage-size 5Gi --storage-path /var/lib/postgresql/data
3036 coderun deploy mysql:8 --name my-mysql --tcp-port 3306 --storage-size 10Gi --storage-path /var/lib/mysql
3137 coderun deploy nginx:latest --name web-server --http-port 80 --storage-size 1Gi --storage-path /usr/share/nginx/html` ,
32- Args : cobra .ExactArgs (1 ),
33- Run : runDeploy ,
38+ Args : func (cmd * cobra.Command , args []string ) error {
39+ // If --build is specified, IMAGE argument is optional
40+ if buildContext != "" {
41+ return cobra .MaximumNArgs (0 )(cmd , args )
42+ }
43+ // Otherwise, IMAGE argument is required
44+ return cobra .ExactArgs (1 )(cmd , args )
45+ },
46+ Run : runDeploy ,
3447}
3548
3649var (
4356 appName string
4457 persistentVolumeSize string
4558 persistentVolumeMountPath string
59+ // Build flags
60+ buildContext string
61+ dockerfilePath string
4662)
4763
4864func init () {
@@ -60,6 +76,10 @@ func init() {
6076 // Persistent storage flags
6177 deployCmd .Flags ().StringVar (& persistentVolumeSize , "storage-size" , "" , "Size of persistent volume (e.g., '1Gi', '500Mi', '10Gi')" )
6278 deployCmd .Flags ().StringVar (& persistentVolumeMountPath , "storage-path" , "" , "Path where to mount the volume (e.g., '/data', '/var/lib/mysql')" )
79+
80+ // Build flags
81+ deployCmd .Flags ().StringVar (& buildContext , "build" , "" , "Build from source. Specify the build context directory (e.g., './my-app' or '.')" )
82+ deployCmd .Flags ().StringVar (& dockerfilePath , "dockerfile" , "Dockerfile" , "Path to Dockerfile relative to build context (default: 'Dockerfile')" )
6383}
6484
6585// parseValidationError tries to parse backend validation errors and return user-friendly messages
@@ -133,7 +153,18 @@ func parseValidationError(errorMsg string) string {
133153}
134154
135155func runDeploy (cmd * cobra.Command , args []string ) {
136- image := args [0 ]
156+ var image string
157+
158+ // Determine if we're building from source or deploying an existing image
159+ isBuild := buildContext != ""
160+
161+ if ! isBuild {
162+ if len (args ) == 0 {
163+ fmt .Println ("Either specify an IMAGE to deploy or use --build to build from source" )
164+ os .Exit (1 )
165+ }
166+ image = args [0 ]
167+ }
137168
138169 // Load config
139170 config , err := utils .LoadConfig ()
@@ -244,6 +275,74 @@ func runDeploy(cmd *cobra.Command, args []string) {
244275 fmt .Printf ("Loaded %d environment variables from %s\n " , len (envVars ), envFile )
245276 }
246277
278+ // Create client
279+ apiClient := client .NewClient (config .BaseURL )
280+ apiClient .SetToken (config .AccessToken )
281+
282+ // Handle build from source
283+ if isBuild {
284+ fmt .Printf ("Building from source in %s...\n " , buildContext )
285+
286+ // Validate build context
287+ if _ , err := os .Stat (buildContext ); os .IsNotExist (err ) {
288+ fmt .Printf ("Build context directory does not exist: %s\n " , buildContext )
289+ os .Exit (1 )
290+ }
291+
292+ // Validate Dockerfile
293+ if err := utils .ValidateDockerfile (buildContext , dockerfilePath ); err != nil {
294+ fmt .Printf ("Error: %v\n " , err )
295+ os .Exit (1 )
296+ }
297+
298+ // Create build context archive
299+ contextArchivePath := utils .GenerateBuildContextPath (appName )
300+ defer os .Remove (contextArchivePath ) // Clean up
301+
302+ fmt .Printf ("Creating build context archive...\n " )
303+ if err := utils .CreateBuildContext (buildContext , contextArchivePath ); err != nil {
304+ fmt .Printf ("Error creating build context: %v\n " , err )
305+ os .Exit (1 )
306+ }
307+
308+ // Upload and start build
309+ fmt .Printf ("Uploading build context and starting build...\n " )
310+ buildResp , err := apiClient .CreateBuild (contextArchivePath , appName , dockerfilePath )
311+ if err != nil {
312+ userFriendlyError := parseValidationError (err .Error ())
313+ fmt .Printf ("Build failed: %s\n " , userFriendlyError )
314+ os .Exit (1 )
315+ }
316+
317+ fmt .Printf ("✅ Build started successfully!\n " )
318+ fmt .Printf ("Build ID: %s\n " , buildResp .ID )
319+ fmt .Printf ("Status: %s\n " , buildResp .Status )
320+ fmt .Printf ("Image URI: %s\n " , buildResp .ImageURI )
321+
322+ // Wait for build to complete
323+ fmt .Printf ("Waiting for build to complete...\n " )
324+ for {
325+ time .Sleep (5 * time .Second )
326+
327+ status , err := apiClient .GetBuildStatus (buildResp .ID )
328+ if err != nil {
329+ fmt .Printf ("Error checking build status: %v\n " , err )
330+ os .Exit (1 )
331+ }
332+
333+ fmt .Printf ("Build status: %s\n " , status .Status )
334+
335+ if status .Status == "completed" {
336+ fmt .Printf ("✅ Build completed successfully!\n " )
337+ image = status .ImageURI
338+ break
339+ } else if status .Status == "failed" {
340+ fmt .Printf ("❌ Build failed!\n " )
341+ os .Exit (1 )
342+ }
343+ }
344+ }
345+
247346 // Create deployment request
248347 deployReq := client.DeploymentCreate {
249348 AppName : appName ,
@@ -270,17 +369,20 @@ func runDeploy(cmd *cobra.Command, args []string) {
270369 deployReq .TCPPort = & tcpPort
271370 }
272371
273- // Create client and deploy
274- apiClient := client .NewClient (config .BaseURL )
275- apiClient .SetToken (config .AccessToken )
372+ // Deploy the application
373+ if isBuild {
374+ fmt .Printf ("Deploying built image %s...\n " , image )
375+ } else {
376+ fmt .Printf ("Deploying %s...\n " , image )
377+ }
276378
277- fmt .Printf ("Deploying %s...\n " , image )
278379 if httpPort > 0 {
279380 fmt .Println ("ℹ️ Note: Deploy with HTTP port may take several minutes (waiting for TLS certificate)" )
280381 }
281382 if tcpPort > 0 {
282383 fmt .Println ("ℹ️ Note: Deploy with TCP port will be available in the NodePort range (30000-32767)" )
283384 }
385+
284386 deployment , err := apiClient .CreateDeployment (& deployReq )
285387 if err != nil {
286388 userFriendlyError := parseValidationError (err .Error ())
@@ -318,4 +420,8 @@ func runDeploy(cmd *cobra.Command, args []string) {
318420
319421 fmt .Printf ("Status: %s\n " , deployment .Status )
320422 fmt .Printf ("Created: %s\n " , deployment .CreatedAt .Format ("2006-01-02 15:04:05" ))
423+
424+ if isBuild {
425+ fmt .Println ("\n 🚀 Successfully built and deployed from source!" )
426+ }
321427}
0 commit comments