github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocrunner/consul_http_sock_hook.go (about)

     1  package allocrunner
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	hclog "github.com/hashicorp/go-hclog"
    12  	"github.com/hashicorp/nomad/client/allocdir"
    13  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/hashicorp/nomad/nomad/structs/config"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  func tgFirstNetworkIsBridge(tg *structs.TaskGroup) bool {
    20  	if len(tg.Networks) < 1 || tg.Networks[0].Mode != "bridge" {
    21  		return false
    22  	}
    23  	return true
    24  }
    25  
    26  const (
    27  	consulHTTPSocketHookName = "consul_http_socket"
    28  )
    29  
    30  type consulHTTPSockHook struct {
    31  	logger hclog.Logger
    32  
    33  	// lock synchronizes proxy and alloc which may be mutated and read concurrently
    34  	// via Prerun, Update, and Postrun.
    35  	lock  sync.Mutex
    36  	alloc *structs.Allocation
    37  	proxy *httpSocketProxy
    38  }
    39  
    40  func newConsulHTTPSocketHook(logger hclog.Logger, alloc *structs.Allocation, allocDir *allocdir.AllocDir, config *config.ConsulConfig) *consulHTTPSockHook {
    41  	return &consulHTTPSockHook{
    42  		alloc:  alloc,
    43  		proxy:  newHTTPSocketProxy(logger, allocDir, config),
    44  		logger: logger.Named(consulHTTPSocketHookName),
    45  	}
    46  }
    47  
    48  func (*consulHTTPSockHook) Name() string {
    49  	return consulHTTPSocketHookName
    50  }
    51  
    52  // shouldRun returns true if the alloc contains at least one connect native
    53  // task and has a network configured in bridge mode
    54  //
    55  // todo(shoenig): what about CNI networks?
    56  func (h *consulHTTPSockHook) shouldRun() bool {
    57  	tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
    58  
    59  	// we must be in bridge networking and at least one connect native task
    60  	if !tgFirstNetworkIsBridge(tg) {
    61  		return false
    62  	}
    63  
    64  	for _, service := range tg.Services {
    65  		if service.Connect.IsNative() {
    66  			return true
    67  		}
    68  	}
    69  	return false
    70  }
    71  
    72  func (h *consulHTTPSockHook) Prerun() error {
    73  	h.lock.Lock()
    74  	defer h.lock.Unlock()
    75  
    76  	if !h.shouldRun() {
    77  		return nil
    78  	}
    79  
    80  	return h.proxy.run(h.alloc)
    81  }
    82  
    83  func (h *consulHTTPSockHook) Update(req *interfaces.RunnerUpdateRequest) error {
    84  	h.lock.Lock()
    85  	defer h.lock.Unlock()
    86  
    87  	h.alloc = req.Alloc
    88  
    89  	if !h.shouldRun() {
    90  		return nil
    91  	}
    92  
    93  	return h.proxy.run(h.alloc)
    94  }
    95  
    96  func (h *consulHTTPSockHook) Postrun() error {
    97  	h.lock.Lock()
    98  	defer h.lock.Unlock()
    99  
   100  	if err := h.proxy.stop(); err != nil {
   101  		// Only log a failure to stop, worst case is the proxy leaks a goroutine.
   102  		h.logger.Warn("error stopping Consul HTTP proxy", "error", err)
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  type httpSocketProxy struct {
   109  	logger   hclog.Logger
   110  	allocDir *allocdir.AllocDir
   111  	config   *config.ConsulConfig
   112  
   113  	ctx     context.Context
   114  	cancel  func()
   115  	doneCh  chan struct{}
   116  	runOnce bool
   117  }
   118  
   119  func newHTTPSocketProxy(logger hclog.Logger, allocDir *allocdir.AllocDir, config *config.ConsulConfig) *httpSocketProxy {
   120  	ctx, cancel := context.WithCancel(context.Background())
   121  	return &httpSocketProxy{
   122  		logger:   logger,
   123  		allocDir: allocDir,
   124  		config:   config,
   125  		ctx:      ctx,
   126  		cancel:   cancel,
   127  		doneCh:   make(chan struct{}),
   128  	}
   129  }
   130  
   131  // run the httpSocketProxy for the given allocation.
   132  //
   133  // Assumes locking done by the calling alloc runner.
   134  func (p *httpSocketProxy) run(alloc *structs.Allocation) error {
   135  	// Only run once.
   136  	if p.runOnce {
   137  		return nil
   138  	}
   139  
   140  	// Never restart.
   141  	select {
   142  	case <-p.doneCh:
   143  		p.logger.Trace("consul http socket proxy already shutdown; exiting")
   144  		return nil
   145  	case <-p.ctx.Done():
   146  		p.logger.Trace("consul http socket proxy already done; exiting")
   147  		return nil
   148  	default:
   149  	}
   150  
   151  	// consul http dest addr
   152  	destAddr := p.config.Addr
   153  	if destAddr == "" {
   154  		return errors.New("consul address must be set on nomad client")
   155  	}
   156  
   157  	hostHTTPSockPath := filepath.Join(p.allocDir.AllocDir, allocdir.AllocHTTPSocket)
   158  	if err := maybeRemoveOldSocket(hostHTTPSockPath); err != nil {
   159  		return err
   160  	}
   161  
   162  	listener, err := net.Listen("unix", hostHTTPSockPath)
   163  	if err != nil {
   164  		return errors.Wrap(err, "unable to create unix socket for Consul HTTP endpoint")
   165  	}
   166  
   167  	// The Consul HTTP socket should be usable by all users in case a task is
   168  	// running as a non-privileged user. Unix does not allow setting domain
   169  	// socket permissions when creating the file, so we must manually call
   170  	// chmod afterwards.
   171  	if err := os.Chmod(hostHTTPSockPath, os.ModePerm); err != nil {
   172  		return errors.Wrap(err, "unable to set permissions on unix socket")
   173  	}
   174  
   175  	go func() {
   176  		proxy(p.ctx, p.logger, destAddr, listener)
   177  		p.cancel()
   178  		close(p.doneCh)
   179  	}()
   180  
   181  	p.runOnce = true
   182  	return nil
   183  }
   184  
   185  func (p *httpSocketProxy) stop() error {
   186  	p.cancel()
   187  
   188  	// if proxy was never run, no need to wait before shutdown
   189  	if !p.runOnce {
   190  		return nil
   191  	}
   192  
   193  	select {
   194  	case <-p.doneCh:
   195  	case <-time.After(socketProxyStopWaitTime):
   196  		return errSocketProxyTimeout
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  func maybeRemoveOldSocket(socketPath string) error {
   203  	_, err := os.Stat(socketPath)
   204  	if err == nil {
   205  		if err = os.Remove(socketPath); err != nil {
   206  			return errors.Wrap(err, "unable to remove existing unix socket")
   207  		}
   208  	}
   209  	return nil
   210  }