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 }