@ -12,30 +12,18 @@ import (
"github.com/containerd/console"
"github.com/docker/buildx/controller/control"
controllererrors "github.com/docker/buildx/controller/errdefs"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/monitor/commands"
"github.com/docker/buildx/monitor/types"
"github.com/docker/buildx/util/ioset"
"github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/identity"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/term"
)
const helpMessage = `
Available commands are :
reload reloads the context and build it .
rollback re - runs the interactive container with initial rootfs contents .
list list buildx sessions .
attach attach to a buildx server or a process in the container .
exec execute a process in the interactive container .
ps list processes invoked by "exec" . Use "attach" to attach IO to that process .
disconnect disconnect a client from a buildx server . Specific session ID can be specified an arg .
kill kill buildx server .
exit exits monitor .
help shows this message .
`
// RunMonitor provides an interactive session for running and managing containers via specified IO.
func RunMonitor ( ctx context . Context , curRef string , options * controllerapi . BuildOptions , invokeConfig controllerapi . InvokeConfig , c control . BuildxController , stdin io . ReadCloser , stdout io . WriteCloser , stderr console . File , progress * progress . Printer ) error {
defer func ( ) {
@ -75,7 +63,8 @@ func RunMonitor(ctx context.Context, curRef string, options *controllerapi.Build
invokeForwarder := ioset . NewForwarder ( )
invokeForwarder . SetIn ( & containerIn )
m := & monitor {
invokeIO : invokeForwarder ,
controllerRef : newControllerRef ( c , curRef ) ,
invokeIO : invokeForwarder ,
muxIO : ioset . NewMuxIO ( ioset . In {
Stdin : io . NopCloser ( stdin ) ,
Stdout : nopCloser { stdout } ,
@ -87,16 +76,30 @@ func RunMonitor(ctx context.Context, curRef string, options *controllerapi.Build
}
return "Switched IO\n"
} ) ,
invokeFunc : c . Invoke ,
}
// Start container automatically
fmt . Fprintf ( stdout , "Launching interactive container. Press Ctrl-a-c to switch to monitor console\n" )
invokeConfig . Rollback = false
invokeConfig . Initial = false
id := m . rollback( ctx , curRef , invokeConfig )
id := m . Rollback( ctx , invokeConfig )
fmt . Fprintf ( stdout , "Interactive container was restarted with process %q. Press Ctrl-a-c to switch to the new container\n" , id )
registeredCommands := map [ string ] types . Command {
"reload" : commands . NewReloadCmd ( m , stdout , progress , options , invokeConfig ) ,
"rollback" : commands . NewRollbackCmd ( m , invokeConfig , stdout ) ,
"list" : commands . NewListCmd ( m , stdout ) ,
"disconnect" : commands . NewDisconnectCmd ( m ) ,
"kill" : commands . NewKillCmd ( m ) ,
"attach" : commands . NewAttachCmd ( m , stdout ) ,
"exec" : commands . NewExecCmd ( m , invokeConfig , stdout ) ,
"ps" : commands . NewPsCmd ( m , stdout ) ,
}
additionalHelpMessages := map [ string ] string {
"help" : "shows this message" ,
"exit" : "exits monitor" ,
}
// Serve monitor commands
monitorForwarder := ioset . NewForwarder ( )
monitorForwarder . SetIn ( & monitorIn )
@ -126,174 +129,29 @@ func RunMonitor(ctx context.Context, curRef string, options *controllerapi.Build
if len ( args ) == 0 {
continue
}
// Builtin commands
switch args [ 0 ] {
case "" :
// nop
case "reload" :
var bo * controllerapi . BuildOptions
if curRef != "" {
// Rebuilding an existing session; Restore the build option used for building this session.
res , err := c . Inspect ( ctx , curRef )
if err != nil {
fmt . Printf ( "failed to inspect the current build session: %v\n" , err )
} else {
bo = res . Options
}
} else {
bo = options
}
if bo == nil {
fmt . Println ( "reload: no build option is provided" )
continue
}
if curRef != "" {
if err := c . Disconnect ( ctx , curRef ) ; err != nil {
fmt . Println ( "disconnect error" , err )
}
}
var resultUpdated bool
progress . Unpause ( )
ref , _ , err := c . Build ( ctx , * bo , nil , progress ) // TODO: support stdin, hold build ref
progress . Pause ( )
if err != nil {
var be * controllererrors . BuildError
if errors . As ( err , & be ) {
curRef = be . Ref
resultUpdated = true
} else {
fmt . Printf ( "failed to reload: %v\n" , err )
}
} else {
curRef = ref
resultUpdated = true
}
if resultUpdated {
// rollback the running container with the new result
id := m . rollback ( ctx , curRef , invokeConfig )
fmt . Fprintf ( stdout , "Interactive container was restarted with process %q. Press Ctrl-a-c to switch to the new container\n" , id )
}
case "rollback" :
cfg := invokeConfig
if len ( args ) >= 2 {
cmds := args [ 1 : ]
if cmds [ 0 ] == "--init" {
cfg . Initial = true
cmds = cmds [ 1 : ]
}
if len ( cmds ) > 0 {
cfg . Entrypoint = [ ] string { cmds [ 0 ] }
cfg . Cmd = cmds [ 1 : ]
}
}
id := m . rollback ( ctx , curRef , cfg )
fmt . Fprintf ( stdout , "Interactive container was restarted with process %q. Press Ctrl-a-c to switch to the new container\n" , id )
case "list" :
refs , err := c . List ( ctx )
if err != nil {
fmt . Printf ( "failed to list: %v\n" , err )
}
sort . Strings ( refs )
tw := tabwriter . NewWriter ( stdout , 1 , 8 , 1 , '\t' , 0 )
fmt . Fprintln ( tw , "ID\tCURRENT_SESSION" )
for _ , k := range refs {
fmt . Fprintf ( tw , "%-20s\t%v\n" , k , k == curRef )
}
tw . Flush ( )
case "disconnect" :
target := curRef
if len ( args ) >= 2 {
target = args [ 1 ]
}
isProcess , err := isProcessID ( ctx , c , curRef , target )
if err == nil && isProcess {
if err := c . DisconnectProcess ( ctx , curRef , target ) ; err != nil {
fmt . Printf ( "disconnect process failed %v\n" , target )
continue
}
continue
}
if err := c . Disconnect ( ctx , target ) ; err != nil {
fmt . Println ( "disconnect error" , err )
}
case "kill" :
if err := c . Kill ( ctx ) ; err != nil {
fmt . Printf ( "failed to kill: %v\n" , err )
}
case "attach" :
if len ( args ) < 2 {
fmt . Println ( "attach: server name must be passed" )
continue
}
ref := args [ 1 ]
var id string
isProcess , err := isProcessID ( ctx , c , curRef , ref )
if err == nil && isProcess {
m . attach ( ctx , curRef , ref )
id = ref
}
if id == "" {
refs , err := c . List ( ctx )
if err != nil {
fmt . Printf ( "failed to get the list of sessions: %v\n" , err )
continue
}
found := false
for _ , s := range refs {
if s == ref {
found = true
break
}
}
if ! found {
fmt . Printf ( "unknown ID: %q\n" , ref )
continue
}
if m . invokeCancel != nil {
m . invokeCancel ( ) // Finish existing attach
}
curRef = ref
}
fmt . Fprintf ( stdout , "Attached to process %q. Press Ctrl-a-c to switch to the new container\n" , id )
case "exec" :
if len ( args ) < 2 {
fmt . Println ( "attach: command must be passed" )
continue
}
if curRef == "" {
fmt . Println ( "attach to a session first" )
continue
}
cfg := controllerapi . InvokeConfig {
Entrypoint : [ ] string { args [ 1 ] } ,
Cmd : args [ 2 : ] ,
// TODO: support other options as well via flags
Env : invokeConfig . Env ,
User : invokeConfig . User ,
Cwd : invokeConfig . Cwd ,
Tty : true ,
}
pid := m . exec ( ctx , curRef , cfg )
fmt . Fprintf ( stdout , "Process %q started. Press Ctrl-a-c to switch to that process.\n" , pid )
case "ps" :
plist , err := c . ListProcesses ( ctx , curRef )
if err != nil {
fmt . Println ( "cannot list process:" , err )
continue
}
tw := tabwriter . NewWriter ( stdout , 1 , 8 , 1 , '\t' , 0 )
fmt . Fprintln ( tw , "PID\tCURRENT_SESSION\tCOMMAND" )
for _ , p := range plist {
fmt . Fprintf ( tw , "%-20s\t%v\t%v\n" , p . ProcessID , p . ProcessID == m . attachedPid . Load ( ) , append ( p . InvokeConfig . Entrypoint , p . InvokeConfig . Cmd ... ) )
}
tw . Flush ( )
continue
case "exit" :
return
case "help" :
fmt . Fprint ( stdout , helpMessage )
printHelpMessage ( stdout , registeredCommands , additionalHelpMessages )
continue
default :
fmt . Printf ( "unknown command: %q\n" , l )
fmt . Fprint ( stdout , helpMessage )
}
// Registered commands
cmdname := args [ 0 ]
if cm , ok := registeredCommands [ cmdname ] ; ok {
if err := cm . Exec ( ctx , args ) ; err != nil {
fmt . Fprintf ( stdout , "%s: %v\n" , cmdname , err )
}
} else {
fmt . Fprintf ( stdout , "monitor: unknown command: %q\n" , l )
printHelpMessage ( stdout , registeredCommands , additionalHelpMessages )
}
}
} ( )
@ -310,41 +168,88 @@ func RunMonitor(ctx context.Context, curRef string, options *controllerapi.Build
}
}
func printHelpMessage ( out io . Writer , registeredCommands map [ string ] types . Command , additional map [ string ] string ) {
var names [ ] string
for name := range registeredCommands {
names = append ( names , name )
}
for name := range additional {
names = append ( names , name )
}
sort . Strings ( names )
fmt . Fprint ( out , "Available commands are:\n" )
w := new ( tabwriter . Writer )
w . Init ( out , 0 , 8 , 0 , '\t' , 0 )
for _ , name := range names {
var mes string
if c , ok := registeredCommands [ name ] ; ok {
mes = c . Info ( ) . HelpMessage
} else if m , ok := additional [ name ] ; ok {
mes = m
} else {
continue
}
fmt . Fprintln ( w , " " + name + "\t" + mes )
}
w . Flush ( )
}
type readWriter struct {
io . Reader
io . Writer
}
type monitor struct {
* controllerRef
muxIO * ioset . MuxIO
invokeIO * ioset . Forwarder
invokeFunc func ( ctx context . Context , ref , pid string , cfg controllerapi . InvokeConfig , in io . ReadCloser , out io . WriteCloser , err io . WriteCloser ) error
invokeCancel func ( )
attachedPid atomic . Value
}
func ( m * monitor ) rollback ( ctx context . Context , ref string , cfg controllerapi . InvokeConfig ) string {
func ( m * monitor ) DisconnectSession ( ctx context . Context , targetID string ) error {
return m . controllerRef . raw ( ) . Disconnect ( ctx , targetID )
}
func ( m * monitor ) AttachSession ( ref string ) {
m . controllerRef . ref . Store ( ref )
}
func ( m * monitor ) AttachedSessionID ( ) string {
return m . controllerRef . ref . Load ( ) . ( string )
}
func ( m * monitor ) Rollback ( ctx context . Context , cfg controllerapi . InvokeConfig ) string {
pid := identity . NewID ( )
cfg1 := cfg
cfg1 . Rollback = true
return m . startInvoke ( ctx , ref , pid , cfg1 )
return m . startInvoke ( ctx , pid, cfg1 )
}
func ( m * monitor ) exec ( ctx context . Context , ref string , cfg controllerapi . InvokeConfig ) string {
return m . startInvoke ( ctx , ref , identity . NewID ( ) , cfg )
func ( m * monitor ) E xec( ctx context . Context , cfg controllerapi . InvokeConfig ) string {
return m . startInvoke ( ctx , identity. NewID ( ) , cfg )
}
func ( m * monitor ) attach ( ctx context . Context , ref , pid string ) {
m . startInvoke ( ctx , ref , pid , controllerapi . InvokeConfig { } )
func ( m * monitor ) A ttach( ctx context . Context , pid string ) {
m . startInvoke ( ctx , pid, controllerapi . InvokeConfig { } )
}
func ( m * monitor ) close ( ) {
func ( m * monitor ) Detach ( ) {
if m . invokeCancel != nil {
m . invokeCancel ( )
m . invokeCancel ( ) // Finish existing attach
}
}
func ( m * monitor ) startInvoke ( ctx context . Context , ref , pid string , cfg controllerapi . InvokeConfig ) string {
func ( m * monitor ) AttachedPID ( ) string {
return m . attachedPid . Load ( ) . ( string )
}
func ( m * monitor ) close ( ) {
m . Detach ( )
}
func ( m * monitor ) startInvoke ( ctx context . Context , pid string , cfg controllerapi . InvokeConfig ) string {
if m . invokeCancel != nil {
m . invokeCancel ( ) // Finish existing attach
}
@ -353,7 +258,7 @@ func (m *monitor) startInvoke(ctx context.Context, ref, pid string, cfg controll
}
go func ( ) {
// Start a new invoke
if err := m . invoke ( ctx , ref, pid, cfg ) ; err != nil {
if err := m . invoke ( ctx , pid, cfg ) ; err != nil {
logrus . Debugf ( "invoke error: %v" , err )
}
if pid == m . attachedPid . Load ( ) {
@ -364,13 +269,13 @@ func (m *monitor) startInvoke(ctx context.Context, ref, pid string, cfg controll
return pid
}
func ( m * monitor ) invoke ( ctx context . Context , ref, pid string , cfg controllerapi . InvokeConfig ) error {
func ( m * monitor ) invoke ( ctx context . Context , pid string , cfg controllerapi . InvokeConfig ) error {
m . muxIO . Enable ( 1 )
defer m . muxIO . Disable ( 1 )
if err := m . muxIO . SwitchTo ( 1 ) ; err != nil {
return errors . Errorf ( "failed to switch to process IO: %v" , err )
}
if ref == "" {
if m. AttachedSessionID ( ) == "" {
return nil
}
invokeCtx , invokeCancel := context . WithCancel ( ctx )
@ -390,27 +295,89 @@ func (m *monitor) invoke(ctx context.Context, ref, pid string, cfg controllerapi
defer invokeCancelAndDetachFn ( )
m . invokeCancel = invokeCancelAndDetachFn
err := m . invoke Func ( invokeCtx , ref , pid , cfg , containerIn . Stdin , containerIn . Stdout , containerIn . Stderr )
err := m . controllerRef . invoke ( invokeCtx , pid , cfg , containerIn . Stdin , containerIn . Stdout , containerIn . Stderr )
close ( waitInvokeDoneCh )
return err
}
type nopCloser struct {
io . Writer
func newControllerRef ( c control . BuildxController , ref string ) * controllerRef {
cr := & controllerRef { c : c }
cr . ref . Store ( ref )
return cr
}
func ( c nopCloser ) Close ( ) error { return nil }
type controllerRef struct {
c control . BuildxController
ref atomic . Value
}
func isProcessID ( ctx context . Context , c control . BuildxController , curRef , ref string ) ( bool , error ) {
infos , err := c . ListProcesses ( ctx , curRef )
func ( c * controllerRef ) raw ( ) control . BuildxController {
return c . c
}
func ( c * controllerRef ) getRef ( ) ( string , error ) {
ref := c . ref . Load ( )
if ref == "" {
return "" , errors . Errorf ( "client is not attached to a session" )
}
return ref . ( string ) , nil
}
func ( c * controllerRef ) Build ( ctx context . Context , options controllerapi . BuildOptions , in io . ReadCloser , progress progress . Writer ) ( ref string , resp * client . SolveResponse , err error ) {
return c . c . Build ( ctx , options , in , progress )
}
func ( c * controllerRef ) invoke ( ctx context . Context , pid string , options controllerapi . InvokeConfig , ioIn io . ReadCloser , ioOut io . WriteCloser , ioErr io . WriteCloser ) error {
ref , err := c . getRef ( )
if err != nil {
return false , err
return err
}
for _ , p := range infos {
if p . ProcessID == ref {
return true , nil
}
return c . c . Invoke ( ctx , ref , pid , options , ioIn , ioOut , ioErr )
}
func ( c * controllerRef ) Kill ( ctx context . Context ) error {
return c . c . Kill ( ctx )
}
func ( c * controllerRef ) List ( ctx context . Context ) ( refs [ ] string , _ error ) {
return c . c . List ( ctx )
}
func ( c * controllerRef ) ListProcesses ( ctx context . Context ) ( infos [ ] * controllerapi . ProcessInfo , retErr error ) {
ref , err := c . getRef ( )
if err != nil {
return nil , err
}
return c . c . ListProcesses ( ctx , ref )
}
func ( c * controllerRef ) DisconnectProcess ( ctx context . Context , pid string ) error {
ref , err := c . getRef ( )
if err != nil {
return err
}
return c . c . DisconnectProcess ( ctx , ref , pid )
}
func ( c * controllerRef ) Inspect ( ctx context . Context ) ( * controllerapi . InspectResponse , error ) {
ref , err := c . getRef ( )
if err != nil {
return nil , err
}
return false , nil
return c . c . Inspect ( ctx , ref )
}
func ( c * controllerRef ) Disconnect ( ctx context . Context ) error {
ref , err := c . getRef ( )
if err != nil {
return err
}
return c . c . Disconnect ( ctx , ref )
}
type nopCloser struct {
io . Writer
}
func ( c nopCloser ) Close ( ) error { return nil }