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 }