github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/controller/remote/controller.go (about)

     1  //go:build linux
     2  
     3  package remote
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"os"
    11  	"os/exec"
    12  	"os/signal"
    13  	"path/filepath"
    14  	"strconv"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/containerd/log"
    19  	"github.com/docker/buildx/build"
    20  	cbuild "github.com/docker/buildx/controller/build"
    21  	"github.com/docker/buildx/controller/control"
    22  	controllerapi "github.com/docker/buildx/controller/pb"
    23  	"github.com/docker/buildx/util/confutil"
    24  	"github.com/docker/buildx/util/progress"
    25  	"github.com/docker/buildx/version"
    26  	"github.com/docker/cli/cli/command"
    27  	"github.com/moby/buildkit/client"
    28  	"github.com/moby/buildkit/util/grpcerrors"
    29  	"github.com/pelletier/go-toml"
    30  	"github.com/pkg/errors"
    31  	"github.com/sirupsen/logrus"
    32  	"github.com/spf13/cobra"
    33  	"google.golang.org/grpc"
    34  )
    35  
    36  const (
    37  	serveCommandName = "_INTERNAL_SERVE"
    38  )
    39  
    40  var (
    41  	defaultLogFilename    = fmt.Sprintf("buildx.%s.log", version.Revision)
    42  	defaultSocketFilename = fmt.Sprintf("buildx.%s.sock", version.Revision)
    43  	defaultPIDFilename    = fmt.Sprintf("buildx.%s.pid", version.Revision)
    44  )
    45  
    46  type serverConfig struct {
    47  	// Specify buildx server root
    48  	Root string `toml:"root"`
    49  
    50  	// LogLevel sets the logging level [trace, debug, info, warn, error, fatal, panic]
    51  	LogLevel string `toml:"log_level"`
    52  
    53  	// Specify file to output buildx server log
    54  	LogFile string `toml:"log_file"`
    55  }
    56  
    57  func NewRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts control.ControlOptions, logger progress.SubLogger) (control.BuildxController, error) {
    58  	rootDir := opts.Root
    59  	if rootDir == "" {
    60  		rootDir = rootDataDir(dockerCli)
    61  	}
    62  	serverRoot := filepath.Join(rootDir, "shared")
    63  
    64  	// connect to buildx server if it is already running
    65  	ctx2, cancel := context.WithTimeout(ctx, 1*time.Second)
    66  	c, err := newBuildxClientAndCheck(ctx2, filepath.Join(serverRoot, defaultSocketFilename))
    67  	cancel()
    68  	if err != nil {
    69  		if !errors.Is(err, context.DeadlineExceeded) {
    70  			return nil, errors.Wrap(err, "cannot connect to the buildx server")
    71  		}
    72  	} else {
    73  		return &buildxController{c, serverRoot}, nil
    74  	}
    75  
    76  	// start buildx server via subcommand
    77  	err = logger.Wrap("no buildx server found; launching...", func() error {
    78  		launchFlags := []string{}
    79  		if opts.ServerConfig != "" {
    80  			launchFlags = append(launchFlags, "--config", opts.ServerConfig)
    81  		}
    82  		logFile, err := getLogFilePath(dockerCli, opts.ServerConfig)
    83  		if err != nil {
    84  			return err
    85  		}
    86  		wait, err := launch(ctx, logFile, append([]string{serveCommandName}, launchFlags...)...)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		go wait()
    91  
    92  		// wait for buildx server to be ready
    93  		ctx2, cancel = context.WithTimeout(ctx, 10*time.Second)
    94  		c, err = newBuildxClientAndCheck(ctx2, filepath.Join(serverRoot, defaultSocketFilename))
    95  		cancel()
    96  		if err != nil {
    97  			return errors.Wrap(err, "cannot connect to the buildx server")
    98  		}
    99  		return nil
   100  	})
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	return &buildxController{c, serverRoot}, nil
   105  }
   106  
   107  func AddControllerCommands(cmd *cobra.Command, dockerCli command.Cli) {
   108  	cmd.AddCommand(
   109  		serveCmd(dockerCli),
   110  	)
   111  }
   112  
   113  func serveCmd(dockerCli command.Cli) *cobra.Command {
   114  	var serverConfigPath string
   115  	cmd := &cobra.Command{
   116  		Use:    fmt.Sprintf("%s [OPTIONS]", serveCommandName),
   117  		Hidden: true,
   118  		RunE: func(cmd *cobra.Command, args []string) error {
   119  			// Parse config
   120  			config, err := getConfig(dockerCli, serverConfigPath)
   121  			if err != nil {
   122  				return err
   123  			}
   124  			if config.LogLevel == "" {
   125  				logrus.SetLevel(logrus.InfoLevel)
   126  			} else {
   127  				lvl, err := logrus.ParseLevel(config.LogLevel)
   128  				if err != nil {
   129  					return errors.Wrap(err, "failed to prepare logger")
   130  				}
   131  				logrus.SetLevel(lvl)
   132  			}
   133  			logrus.SetFormatter(&logrus.JSONFormatter{
   134  				TimestampFormat: log.RFC3339NanoFixed,
   135  			})
   136  			root, err := prepareRootDir(dockerCli, config)
   137  			if err != nil {
   138  				return err
   139  			}
   140  			pidF := filepath.Join(root, defaultPIDFilename)
   141  			if err := os.WriteFile(pidF, []byte(fmt.Sprintf("%d", os.Getpid())), 0600); err != nil {
   142  				return err
   143  			}
   144  			defer func() {
   145  				if err := os.Remove(pidF); err != nil {
   146  					logrus.Errorf("failed to clean up info file %q: %v", pidF, err)
   147  				}
   148  			}()
   149  
   150  			// prepare server
   151  			b := NewServer(func(ctx context.Context, options *controllerapi.BuildOptions, stdin io.Reader, progress progress.Writer) (*client.SolveResponse, *build.ResultHandle, error) {
   152  				return cbuild.RunBuild(ctx, dockerCli, *options, stdin, progress, true)
   153  			})
   154  			defer b.Close()
   155  
   156  			// serve server
   157  			addr := filepath.Join(root, defaultSocketFilename)
   158  			if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { // avoid EADDRINUSE
   159  				return err
   160  			}
   161  			defer func() {
   162  				if err := os.Remove(addr); err != nil {
   163  					logrus.Errorf("failed to clean up socket %q: %v", addr, err)
   164  				}
   165  			}()
   166  			logrus.Infof("starting server at %q", addr)
   167  			l, err := net.Listen("unix", addr)
   168  			if err != nil {
   169  				return err
   170  			}
   171  			rpc := grpc.NewServer(
   172  				grpc.UnaryInterceptor(grpcerrors.UnaryServerInterceptor),
   173  				grpc.StreamInterceptor(grpcerrors.StreamServerInterceptor),
   174  			)
   175  			controllerapi.RegisterControllerServer(rpc, b)
   176  			doneCh := make(chan struct{})
   177  			errCh := make(chan error, 1)
   178  			go func() {
   179  				defer close(doneCh)
   180  				if err := rpc.Serve(l); err != nil {
   181  					errCh <- errors.Wrapf(err, "error on serving via socket %q", addr)
   182  				}
   183  			}()
   184  
   185  			var s os.Signal
   186  			sigCh := make(chan os.Signal, 1)
   187  			signal.Notify(sigCh, syscall.SIGINT)
   188  			signal.Notify(sigCh, syscall.SIGTERM)
   189  			select {
   190  			case err := <-errCh:
   191  				logrus.Errorf("got error %s, exiting", err)
   192  				return err
   193  			case s = <-sigCh:
   194  				logrus.Infof("got signal %s, exiting", s)
   195  				return nil
   196  			case <-doneCh:
   197  				logrus.Infof("rpc server done, exiting")
   198  				return nil
   199  			}
   200  		},
   201  	}
   202  
   203  	flags := cmd.Flags()
   204  	flags.StringVar(&serverConfigPath, "config", "", "Specify buildx server config file")
   205  	return cmd
   206  }
   207  
   208  func getLogFilePath(dockerCli command.Cli, configPath string) (string, error) {
   209  	config, err := getConfig(dockerCli, configPath)
   210  	if err != nil {
   211  		return "", err
   212  	}
   213  	if config.LogFile == "" {
   214  		root, err := prepareRootDir(dockerCli, config)
   215  		if err != nil {
   216  			return "", err
   217  		}
   218  		return filepath.Join(root, defaultLogFilename), nil
   219  	}
   220  	return config.LogFile, nil
   221  }
   222  
   223  func getConfig(dockerCli command.Cli, configPath string) (*serverConfig, error) {
   224  	var defaultConfigPath bool
   225  	if configPath == "" {
   226  		defaultRoot := rootDataDir(dockerCli)
   227  		configPath = filepath.Join(defaultRoot, "config.toml")
   228  		defaultConfigPath = true
   229  	}
   230  	var config serverConfig
   231  	tree, err := toml.LoadFile(configPath)
   232  	if err != nil && !(os.IsNotExist(err) && defaultConfigPath) {
   233  		return nil, errors.Wrapf(err, "failed to read config %q", configPath)
   234  	} else if err == nil {
   235  		if err := tree.Unmarshal(&config); err != nil {
   236  			return nil, errors.Wrapf(err, "failed to unmarshal config %q", configPath)
   237  		}
   238  	}
   239  	return &config, nil
   240  }
   241  
   242  func prepareRootDir(dockerCli command.Cli, config *serverConfig) (string, error) {
   243  	rootDir := config.Root
   244  	if rootDir == "" {
   245  		rootDir = rootDataDir(dockerCli)
   246  	}
   247  	if rootDir == "" {
   248  		return "", errors.New("buildx root dir must be determined")
   249  	}
   250  	if err := os.MkdirAll(rootDir, 0700); err != nil {
   251  		return "", err
   252  	}
   253  	serverRoot := filepath.Join(rootDir, "shared")
   254  	if err := os.MkdirAll(serverRoot, 0700); err != nil {
   255  		return "", err
   256  	}
   257  	return serverRoot, nil
   258  }
   259  
   260  func rootDataDir(dockerCli command.Cli) string {
   261  	return filepath.Join(confutil.ConfigDir(dockerCli), "controller")
   262  }
   263  
   264  func newBuildxClientAndCheck(ctx context.Context, addr string) (*Client, error) {
   265  	c, err := NewClient(ctx, addr)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	p, v, r, err := c.Version(ctx)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	logrus.Debugf("connected to server (\"%v %v %v\")", p, v, r)
   274  	if !(p == version.Package && v == version.Version && r == version.Revision) {
   275  		return nil, errors.Errorf("version mismatch (client: \"%v %v %v\", server: \"%v %v %v\")", version.Package, version.Version, version.Revision, p, v, r)
   276  	}
   277  	return c, nil
   278  }
   279  
   280  type buildxController struct {
   281  	*Client
   282  	serverRoot string
   283  }
   284  
   285  func (c *buildxController) Kill(ctx context.Context) error {
   286  	pidB, err := os.ReadFile(filepath.Join(c.serverRoot, defaultPIDFilename))
   287  	if err != nil {
   288  		return err
   289  	}
   290  	pid, err := strconv.ParseInt(string(pidB), 10, 64)
   291  	if err != nil {
   292  		return err
   293  	}
   294  	if pid <= 0 {
   295  		return errors.New("no PID is recorded for buildx server")
   296  	}
   297  	p, err := os.FindProcess(int(pid))
   298  	if err != nil {
   299  		return err
   300  	}
   301  	if err := p.Signal(syscall.SIGINT); err != nil {
   302  		return err
   303  	}
   304  	// TODO: Should we send SIGKILL if process doesn't finish?
   305  	return nil
   306  }
   307  
   308  func launch(ctx context.Context, logFile string, args ...string) (func() error, error) {
   309  	// set absolute path of binary, since we set the working directory to the root
   310  	pathname, err := os.Executable()
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	bCmd := exec.CommandContext(ctx, pathname, args...)
   315  	if logFile != "" {
   316  		f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   317  		if err != nil {
   318  			return nil, err
   319  		}
   320  		defer f.Close()
   321  		bCmd.Stdout = f
   322  		bCmd.Stderr = f
   323  	}
   324  	bCmd.Stdin = nil
   325  	bCmd.Dir = "/"
   326  	bCmd.SysProcAttr = &syscall.SysProcAttr{
   327  		Setsid: true,
   328  	}
   329  	if err := bCmd.Start(); err != nil {
   330  		return nil, err
   331  	}
   332  	return bCmd.Wait, nil
   333  }