github.com/hashicorp/packer@v1.14.3/packer/plugin_client.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package packer
     5  
     6  import (
     7  	"bufio"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"log"
    12  	"net"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  	"unicode"
    20  
    21  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    22  	pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
    23  	packerrpc "github.com/hashicorp/packer-plugin-sdk/rpc"
    24  )
    25  
    26  // If this is true, then the "unexpected EOF" panic will not be
    27  // raised throughout the clients.
    28  var Killed = false
    29  
    30  // This is a slice of the "managed" clients which are cleaned up when
    31  // calling Cleanup
    32  var managedClients = make([]*PluginClient, 0, 5)
    33  
    34  // Client handles the lifecycle of a plugin application, determining its
    35  // RPC address, and returning various types of packer interface implementations
    36  // across the multi-process communication layer.
    37  type PluginClient struct {
    38  	config      *PluginClientConfig
    39  	exited      bool
    40  	doneLogging chan struct{}
    41  	l           sync.Mutex
    42  	address     net.Addr
    43  }
    44  
    45  // PluginClientConfig is the configuration used to initialize a new
    46  // plugin client. After being used to initialize a plugin client,
    47  // that configuration must not be modified again.
    48  type PluginClientConfig struct {
    49  	// The unstarted subprocess for starting the plugin.
    50  	Cmd *exec.Cmd
    51  
    52  	// Managed represents if the client should be managed by the
    53  	// plugin package or not. If true, then by calling CleanupClients,
    54  	// it will automatically be cleaned up. Otherwise, the client
    55  	// user is fully responsible for making sure to Kill all plugin
    56  	// clients. By default the client is _not_ managed.
    57  	Managed bool
    58  
    59  	// The minimum and maximum port to use for communicating with
    60  	// the subprocess. If not set, this defaults to 10,000 and 25,000
    61  	// respectively.
    62  	MinPort, MaxPort int
    63  
    64  	// StartTimeout is the timeout to wait for the plugin to say it
    65  	// has started successfully.
    66  	StartTimeout time.Duration
    67  
    68  	// If non-nil, then the stderr of the client will be written to here
    69  	// (as well as the log).
    70  	Stderr io.Writer
    71  }
    72  
    73  // This makes sure all the managed subprocesses are killed and properly
    74  // logged. This should be called before the parent process running the
    75  // plugins exits.
    76  //
    77  // This must only be called _once_.
    78  func CleanupClients() {
    79  	// Set the killed to true so that we don't get unexpected panics
    80  	Killed = true
    81  
    82  	// Kill all the managed clients in parallel and use a WaitGroup
    83  	// to wait for them all to finish up.
    84  	var wg sync.WaitGroup
    85  	for _, client := range managedClients {
    86  		wg.Add(1)
    87  
    88  		go func(client *PluginClient) {
    89  			client.Kill()
    90  			wg.Done()
    91  		}(client)
    92  	}
    93  
    94  	log.Println("waiting for all plugin processes to complete...")
    95  	wg.Wait()
    96  }
    97  
    98  // Creates a new plugin client which manages the lifecycle of an external
    99  // plugin and gets the address for the RPC connection.
   100  //
   101  // The client must be cleaned up at some point by calling Kill(). If
   102  // the client is a managed client (created with NewManagedClient) you
   103  // can just call CleanupClients at the end of your program and they will
   104  // be properly cleaned.
   105  func NewClient(config *PluginClientConfig) (c *PluginClient) {
   106  	if config.MinPort == 0 && config.MaxPort == 0 {
   107  		config.MinPort = 10000
   108  		config.MaxPort = 25000
   109  	}
   110  
   111  	if config.StartTimeout == 0 {
   112  		config.StartTimeout = 1 * time.Minute
   113  	}
   114  
   115  	if config.Stderr == nil {
   116  		config.Stderr = io.Discard
   117  	}
   118  
   119  	c = &PluginClient{config: config}
   120  	if config.Managed {
   121  		managedClients = append(managedClients, c)
   122  	}
   123  
   124  	return
   125  }
   126  
   127  // Tells whether or not the underlying process has exited.
   128  func (c *PluginClient) Exited() bool {
   129  	c.l.Lock()
   130  	defer c.l.Unlock()
   131  	return c.exited
   132  }
   133  
   134  // Returns a builder implementation that is communicating over this
   135  // client. If the client hasn't been started, this will start it.
   136  func (c *PluginClient) Builder() (packersdk.Builder, error) {
   137  	client, err := c.Client()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	return &cmdBuilder{client.Builder(), c}, nil
   143  }
   144  
   145  // Returns a hook implementation that is communicating over this
   146  // client. If the client hasn't been started, this will start it.
   147  func (c *PluginClient) Hook() (packersdk.Hook, error) {
   148  	client, err := c.Client()
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	return &cmdHook{client.Hook(), c}, nil
   154  }
   155  
   156  // Returns a post-processor implementation that is communicating over
   157  // this client. If the client hasn't been started, this will start it.
   158  func (c *PluginClient) PostProcessor() (packersdk.PostProcessor, error) {
   159  	client, err := c.Client()
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	return &cmdPostProcessor{client.PostProcessor(), c}, nil
   165  }
   166  
   167  // Returns a provisioner implementation that is communicating over this
   168  // client. If the client hasn't been started, this will start it.
   169  func (c *PluginClient) Provisioner() (packersdk.Provisioner, error) {
   170  	client, err := c.Client()
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	return &cmdProvisioner{client.Provisioner(), c}, nil
   176  }
   177  
   178  // Returns a data source implementation that is communicating over this
   179  // client. If the client hasn't been started, this will start it.
   180  func (c *PluginClient) Datasource() (packersdk.Datasource, error) {
   181  	client, err := c.Client()
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	return &cmdDatasource{client.Datasource(), c}, nil
   186  }
   187  
   188  // End the executing subprocess (if it is running) and perform any cleanup
   189  // tasks necessary such as capturing any remaining logs and so on.
   190  //
   191  // This method blocks until the process successfully exits.
   192  //
   193  // This method can safely be called multiple times.
   194  func (c *PluginClient) Kill() {
   195  	cmd := c.config.Cmd
   196  
   197  	if cmd.Process == nil {
   198  		return
   199  	}
   200  
   201  	_ = cmd.Process.Kill()
   202  
   203  	// Wait for the client to finish logging so we have a complete log
   204  	<-c.doneLogging
   205  }
   206  
   207  // Starts the underlying subprocess, communicating with it to negotiate
   208  // a port for RPC connections, and returning the address to connect via RPC.
   209  //
   210  // This method is safe to call multiple times. Subsequent calls have no effect.
   211  // Once a client has been started once, it cannot be started again, even if
   212  // it was killed.
   213  func (c *PluginClient) Start() (net.Addr, error) {
   214  	c.l.Lock()
   215  	defer c.l.Unlock()
   216  
   217  	if c.address != nil {
   218  		return c.address, nil
   219  	}
   220  
   221  	c.doneLogging = make(chan struct{})
   222  
   223  	env := []string{
   224  		fmt.Sprintf("%s=%s", pluginsdk.MagicCookieKey, pluginsdk.MagicCookieValue),
   225  		fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort),
   226  		fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
   227  	}
   228  
   229  	stdout_r, stdout_w := io.Pipe()
   230  	stderr_r, stderr_w := io.Pipe()
   231  
   232  	cmd := c.config.Cmd
   233  	cmd.Env = append(cmd.Env, os.Environ()...)
   234  	cmd.Env = append(cmd.Env, env...)
   235  	cmd.Stdin = os.Stdin
   236  	cmd.Stderr = stderr_w
   237  	cmd.Stdout = stdout_w
   238  
   239  	log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args)
   240  	err := cmd.Start()
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	// Make sure the command is properly cleaned up if there is an error
   246  	defer func() {
   247  		r := recover()
   248  
   249  		if err != nil || r != nil {
   250  			_ = cmd.Process.Kill()
   251  		}
   252  
   253  		if r != nil {
   254  			panic(r)
   255  		}
   256  	}()
   257  
   258  	// Start goroutine to wait for process to exit
   259  	exitCh := make(chan struct{})
   260  	go func() {
   261  		// Make sure we close the write end of our stderr/stdout so
   262  		// that the readers send EOF properly.
   263  		defer stderr_w.Close()
   264  		defer stdout_w.Close()
   265  
   266  		// Wait for the command to end.
   267  		_ = cmd.Wait()
   268  
   269  		// Log and make sure to flush the logs write away
   270  		log.Printf("%s: plugin process exited\n", cmd.Path)
   271  		os.Stderr.Sync()
   272  
   273  		// Mark that we exited
   274  		close(exitCh)
   275  
   276  		// Set that we exited, which takes a lock
   277  		c.l.Lock()
   278  		defer c.l.Unlock()
   279  		c.exited = true
   280  	}()
   281  
   282  	// Start goroutine that logs the stderr
   283  	go c.logStderr(stderr_r)
   284  
   285  	// Start a goroutine that is going to be reading the lines
   286  	// out of stdout
   287  	linesCh := make(chan []byte)
   288  	go func() {
   289  		defer close(linesCh)
   290  
   291  		buf := bufio.NewReader(stdout_r)
   292  		for {
   293  			line, err := buf.ReadBytes('\n')
   294  			if line != nil {
   295  				linesCh <- line
   296  			}
   297  
   298  			if err == io.EOF {
   299  				return
   300  			}
   301  		}
   302  	}()
   303  
   304  	// Make sure after we exit we read the lines from stdout forever
   305  	// so they don't block since it is an io.Pipe
   306  	defer func() {
   307  		go func() {
   308  			for range linesCh {
   309  			}
   310  		}()
   311  	}()
   312  
   313  	// Some channels for the next step
   314  	timeout := time.After(c.config.StartTimeout)
   315  
   316  	// Start looking for the address
   317  	log.Printf("Waiting for RPC address for: %s", cmd.Path)
   318  	select {
   319  	case <-timeout:
   320  		err = errors.New("timeout while waiting for plugin to start")
   321  	case <-exitCh:
   322  		err = errors.New("plugin exited before we could connect")
   323  	case lineBytes := <-linesCh:
   324  		// Trim the line and split by "|" in order to get the parts of
   325  		// the output.
   326  		line := strings.TrimSpace(string(lineBytes))
   327  		parts := strings.SplitN(line, "|", 4)
   328  		if len(parts) == 3 {
   329  			// In protocol version 4 and before, the protocol only had a Major
   330  			// version
   331  			err = fmt.Errorf("The protocol of this plugin (protocol version 4 " +
   332  				"and lower) was deprecated, please use a newer version of this plugin." +
   333  				"Or use an older version of Packer (pre 1.7) with this plugin.")
   334  			return nil, err
   335  		}
   336  		if len(parts) < 4 {
   337  			err = fmt.Errorf("Unrecognized remote plugin message: %s", line)
   338  			return nil, err
   339  		}
   340  		pluginMajorAPIVersion, pluginMinorAPIVersion, network, netAddr := parts[0], parts[1], parts[2], parts[3]
   341  
   342  		// Test the API versions
   343  		if pluginMajorAPIVersion != pluginsdk.APIVersionMajor {
   344  			err = fmt.Errorf("Incompatible API MAJOR version with plugin. "+
   345  				"plugin MINOR API version: %s, Ours: %s", pluginMajorAPIVersion, pluginsdk.APIVersionMajor)
   346  			return nil, err
   347  		}
   348  		if pluginMinorAPIVersion > pluginsdk.APIVersionMinor {
   349  			err = fmt.Errorf("Incompatible API MINOR version with plugin. "+
   350  				"plugin MINOR API version: %s, Ours: %s. Please upgrade Packer.", pluginMinorAPIVersion, pluginsdk.APIVersionMinor)
   351  			return nil, err
   352  		}
   353  
   354  		switch network {
   355  		case "tcp":
   356  			c.address, err = net.ResolveTCPAddr("tcp", netAddr)
   357  		case "unix":
   358  			c.address, err = net.ResolveUnixAddr("unix", netAddr)
   359  		default:
   360  			return nil, fmt.Errorf("Unknown address type: %s", network)
   361  		}
   362  		log.Printf("Received %s RPC address for %s: addr is %s", network, cmd.Path, c.address)
   363  	}
   364  
   365  	return c.address, err
   366  }
   367  
   368  func (c *PluginClient) logStderr(r io.Reader) {
   369  	logPrefix := filepath.Base(c.config.Cmd.Path)
   370  	if logPrefix == "packer" {
   371  		// we just called the normal packer binary with the plugin arg.
   372  		// grab the last arg from the list which will match the plugin name.
   373  		logPrefix = c.config.Cmd.Args[len(c.config.Cmd.Args)-1]
   374  	}
   375  
   376  	bufR := bufio.NewReader(r)
   377  	for {
   378  		line, err := bufR.ReadString('\n')
   379  		if line != "" {
   380  			_, _ = c.config.Stderr.Write([]byte(line))
   381  
   382  			line = strings.TrimRightFunc(line, unicode.IsSpace)
   383  
   384  			log.Printf("%s plugin: %s", logPrefix, line)
   385  		}
   386  
   387  		if err == io.EOF {
   388  			break
   389  		}
   390  	}
   391  
   392  	// Flag that we've completed logging for others
   393  	close(c.doneLogging)
   394  }
   395  
   396  func (c *PluginClient) Client() (*packerrpc.Client, error) {
   397  	addr, err := c.Start()
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	conn, err := net.Dial(addr.Network(), addr.String())
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	if tcpConn, ok := conn.(*net.TCPConn); ok {
   408  		// Make sure to set keep alive so that the connection doesn't die
   409  		err = tcpConn.SetKeepAlive(true)
   410  		if err != nil {
   411  			log.Printf("[ERROR] failed to set keepalive for TCP connection to plugin: %s. Some instability may occur.", err)
   412  		}
   413  	}
   414  
   415  	client, err := packerrpc.NewClient(conn)
   416  	if err != nil {
   417  		conn.Close()
   418  		return nil, err
   419  	}
   420  	client.UseProto = PackerUseProto
   421  
   422  	return client, nil
   423  }