@@ -8,10 +8,12 @@ import (
88 "strings"
99 "time"
1010
11+ "github.com/briandowns/spinner"
1112 truenas "github.com/deevus/truenas-go"
1213 "github.com/spf13/cobra"
1314
1415 "github.com/deevus/pixels/internal/cache"
16+ "github.com/deevus/pixels/internal/provision"
1517 "github.com/deevus/pixels/internal/retry"
1618 "github.com/deevus/pixels/internal/ssh"
1719 tnc "github.com/deevus/pixels/internal/truenas"
@@ -66,6 +68,26 @@ func runCreate(cmd *cobra.Command, args []string) error {
6668
6769 logv (cmd , "Config: image=%s cpu=%s memory=%dMiB egress=%s" , image , cpu , memory , egressMode )
6870
71+ // Spinner for non-verbose mode — shows current phase on stderr.
72+ var spin * spinner.Spinner
73+ if ! verbose {
74+ spin = spinner .New (spinner .CharSets [14 ], 100 * time .Millisecond , spinner .WithWriter (cmd .ErrOrStderr ()))
75+ }
76+ setStatus := func (msg string ) {
77+ if spin != nil {
78+ spin .Suffix = " " + msg
79+ if ! spin .Active () {
80+ spin .Start ()
81+ }
82+ }
83+ }
84+ stopSpinner := func () {
85+ if spin != nil && spin .Active () {
86+ spin .Stop ()
87+ }
88+ }
89+ defer stopSpinner ()
90+
6991 // Parse --from flag: "container" or "container:label"
7092 var fromSource , fromLabel string
7193 var tempSnapshot bool
@@ -163,9 +185,9 @@ func runCreate(cmd *cobra.Command, args []string) error {
163185 // (pool.dataset.* APIs can't see .ix-virt managed datasets).
164186 if skipProvision {
165187 if tempSnapshot {
166- fmt .Fprintf ( cmd . ErrOrStderr (), "Cloning from %s...\n " , fromSource )
188+ setStatus ( fmt .Sprintf ( "Cloning from %s..." , fromSource ) )
167189 } else {
168- fmt .Fprintf ( cmd . ErrOrStderr (), "Cloning from %s checkpoint %q...\n " , fromSource , fromLabel )
190+ setStatus ( fmt .Sprintf ( "Cloning from %s checkpoint %q..." , fromSource , fromLabel ) )
169191 }
170192
171193 logv (cmd , "Stopping %s for rootfs replacement..." , containerName (name ))
@@ -197,6 +219,9 @@ func runCreate(cmd *cobra.Command, args []string) error {
197219 }
198220 }
199221
222+ // Compute provisioning steps (devtools, egress) before writing files.
223+ steps := provision .Steps (egressMode , cfg .Provision .DevToolsEnabled ())
224+
200225 // Provision while the container is running (rootfs only mounted when up).
201226 noProvision , _ := cmd .Flags ().GetBool ("no-provision" )
202227 provisionEnabled := cfg .Provision .IsEnabled () && ! noProvision && ! skipProvision
@@ -211,14 +236,17 @@ func runCreate(cmd *cobra.Command, args []string) error {
211236 Egress : egressMode ,
212237 EgressAllow : cfg .Network .Allow ,
213238 }
239+ if len (steps ) > 0 {
240+ provOpts .ProvisionScript = provision .Script (steps )
241+ }
214242 if verbose {
215243 provOpts .Log = cmd .ErrOrStderr ()
216244 }
217245 needsProvision := pubKey != "" || len (cfg .Defaults .DNS ) > 0 ||
218246 len (cfg .Env ) > 0 || provOpts .DevTools
219247
220248 if needsProvision {
221- fmt . Fprintf ( cmd . ErrOrStderr (), "Provisioning...\n " )
249+ setStatus ( "Provisioning..." )
222250 logv (cmd , "SSH key: %v, DNS: %d, Env: %d, DevTools: %v, Egress: %s" ,
223251 pubKey != "" , len (cfg .Defaults .DNS ), len (cfg .Env ), provOpts .DevTools , egressMode )
224252
@@ -255,6 +283,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
255283 timeout = 30 * time .Second
256284 }
257285 if provisionEnabled || skipProvision {
286+ setStatus ("Waiting for SSH..." )
258287 var sshLog io.Writer
259288 if verbose {
260289 sshLog = cmd .ErrOrStderr ()
@@ -269,6 +298,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
269298 cache .Put (name , & cache.Entry {IP : ip , Status : instance .Status })
270299 logv (cmd , "Cached IP=%s status=%s for %s" , ip , instance .Status , name )
271300
301+ stopSpinner ()
272302 elapsed := time .Since (start ).Truncate (100 * time .Millisecond )
273303 fmt .Fprintf (cmd .OutOrStdout (), "Created %s in %s\n " , containerName (name ), elapsed )
274304 fmt .Fprintf (cmd .OutOrStdout (), " Hostname: %s\n " , containerName (name ))
@@ -277,38 +307,18 @@ func runCreate(cmd *cobra.Command, args []string) error {
277307 }
278308 fmt .Fprintf (cmd .OutOrStdout (), " Console: pixels console %s\n " , name )
279309 openConsole , _ := cmd .Flags ().GetBool ("console" )
280- devToolsActive := provisionEnabled && cfg .Provision .DevToolsEnabled ()
281310
282- if devToolsActive && ! openConsole {
283- fmt .Fprintf (cmd .OutOrStdout (), " Dev tools installing in background (sudo journalctl -fu pixels-devtools) \n " )
311+ if len ( steps ) > 0 && ! openConsole {
312+ fmt .Fprintf (cmd .OutOrStdout (), " Status: pixels status %s \n " , name )
284313 }
285314
286315 if openConsole && ip != "" {
287- if devToolsActive {
288- fmt .Fprintf (cmd .ErrOrStderr (), "Waiting for dev tools to finish installing...\n " )
289-
290- // Stream journal output so the user can see progress.
291- var journalCancel context.CancelFunc
292- var done chan struct {}
293- if verbose {
294- var journalCtx context.Context
295- journalCtx , journalCancel = context .WithCancel (ctx )
296- done = make (chan struct {})
297- go func () {
298- defer close (done )
299- ssh .Exec (journalCtx , ip , "root" , cfg .SSH .Key ,
300- []string {"journalctl" , "-fu" , "pixels-devtools" , "--no-pager" , "-o" , "cat" }, nil )
301- }()
302- }
303-
304- if err := ssh .WaitProvisioned (ctx , ip , cfg .SSH .User , cfg .SSH .Key , 10 * time .Minute ); err != nil {
305- fmt .Fprintf (cmd .ErrOrStderr (), "Warning: %v\n " , err )
306- }
307- if journalCancel != nil {
308- journalCancel ()
309- <- done
310- }
311- }
316+ runner := & provision.Runner {Host : ip , User : "root" , KeyPath : cfg .SSH .Key }
317+ runner .WaitProvisioned (ctx , func (status string ) {
318+ setStatus (status )
319+ logv (cmd , "Provision: %s" , status )
320+ })
321+ stopSpinner ()
312322 return ssh .Console (ip , cfg .SSH .User , cfg .SSH .Key , cfg .EnvForward )
313323 }
314324
0 commit comments