github.com/uchennaokeke444/nomad@v0.11.8/nomad/job_endpoint_hook_expose_check.go (about) 1 package nomad 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "github.com/hashicorp/nomad/helper/uuid" 9 "github.com/hashicorp/nomad/nomad/structs" 10 "github.com/pkg/errors" 11 ) 12 13 type jobExposeCheckHook struct{} 14 15 func (jobExposeCheckHook) Name() string { 16 return "expose-check" 17 } 18 19 // Mutate will scan every task group for group-services which have checks defined 20 // that have the Expose field configured, and generate expose path configurations 21 // extrapolated from those check definitions. 22 func (jobExposeCheckHook) Mutate(job *structs.Job) (_ *structs.Job, warnings []error, err error) { 23 for _, tg := range job.TaskGroups { 24 for _, s := range tg.Services { 25 for _, c := range s.Checks { 26 if c.Expose { 27 if exposePath, err := exposePathForCheck(tg, s, c); err != nil { 28 return nil, nil, err 29 } else if exposePath != nil { 30 serviceExposeConfig := serviceExposeConfig(s) 31 // insert only if not already present - required for job 32 // updates which would otherwise create duplicates 33 if !containsExposePath(serviceExposeConfig.Paths, *exposePath) { 34 serviceExposeConfig.Paths = append( 35 serviceExposeConfig.Paths, *exposePath, 36 ) 37 } 38 } 39 } 40 } 41 } 42 } 43 return job, nil, nil 44 } 45 46 // Validate will ensure: 47 // - The job contains valid network configuration for each task group in which 48 // an expose path is configured. The network must be of type bridge mode. 49 // - The check Expose field is configured only for connect-enabled group-services. 50 func (jobExposeCheckHook) Validate(job *structs.Job) (warnings []error, err error) { 51 for _, tg := range job.TaskGroups { 52 // Make sure any group that contains a group-service that enables expose 53 // is configured with one network that is in "bridge" mode. This check 54 // is being done independently of the preceding Connect task injection 55 // hook, because at some point in the future Connect will not require the 56 // use of network namespaces, whereas the use of "expose" does not make 57 // sense without the use of network namespace. 58 if err := tgValidateUseOfBridgeMode(tg); err != nil { 59 return nil, err 60 } 61 // Make sure any group-service that contains a check that enables expose 62 // is connect-enabled and does not specify a custom sidecar task. We only 63 // support the expose feature when using the built-in Envoy integration. 64 if err := tgValidateUseOfCheckExpose(tg); err != nil { 65 return nil, err 66 } 67 } 68 return nil, nil 69 } 70 71 // serviceExposeConfig digs through s to extract the connect sidecar service proxy 72 // expose configuration. It is not required of the user to provide this, so it 73 // is created on demand here as needed in the case where any service check exposes 74 // itself. 75 // 76 // The service, connect, and sidecar_service are assumed not to be nil, as they 77 // are enforced in previous hooks / validation. 78 func serviceExposeConfig(s *structs.Service) *structs.ConsulExposeConfig { 79 if s.Connect.SidecarService.Proxy == nil { 80 s.Connect.SidecarService.Proxy = new(structs.ConsulProxy) 81 } 82 if s.Connect.SidecarService.Proxy.Expose == nil { 83 s.Connect.SidecarService.Proxy.Expose = new(structs.ConsulExposeConfig) 84 } 85 return s.Connect.SidecarService.Proxy.Expose 86 } 87 88 // containsExposePath returns true if path is contained in paths. 89 func containsExposePath(paths []structs.ConsulExposePath, path structs.ConsulExposePath) bool { 90 for _, p := range paths { 91 if p == path { 92 return true 93 } 94 } 95 return false 96 } 97 98 // tgValidateUseOfCheckExpose ensures that any service check in tg making use 99 // of the expose field is within an appropriate context to do so. The check must 100 // be a group level check, and must use the builtin envoy proxy. 101 func tgValidateUseOfCheckExpose(tg *structs.TaskGroup) error { 102 // validation for group services (which must use built-in connect proxy) 103 for _, s := range tg.Services { 104 for _, check := range s.Checks { 105 if check.Expose && !serviceUsesConnectEnvoy(s) { 106 return errors.Errorf( 107 "exposed service check %s->%s->%s requires use of Nomad's builtin Connect proxy", 108 tg.Name, s.Name, check.Name, 109 ) 110 } 111 } 112 } 113 114 // validation for task services (which must not be configured to use Expose) 115 for _, t := range tg.Tasks { 116 for _, s := range t.Services { 117 for _, check := range s.Checks { 118 if check.Expose { 119 return errors.Errorf( 120 "exposed service check %s[%s]->%s->%s is not a task-group service", 121 tg.Name, t.Name, s.Name, check.Name, 122 ) 123 } 124 } 125 } 126 } 127 return nil 128 } 129 130 // tgValidateUseOfBridgeMode ensures there is exactly 1 network configured for 131 // the task group, and that it makes use of "bridge" mode (i.e. enables network 132 // namespaces). 133 func tgValidateUseOfBridgeMode(tg *structs.TaskGroup) error { 134 if tgUsesExposeCheck(tg) { 135 if len(tg.Networks) != 1 { 136 return errors.Errorf("group %q must specify one bridge network for exposing service check(s)", tg.Name) 137 } 138 if tg.Networks[0].Mode != "bridge" { 139 return errors.Errorf("group %q must use bridge network for exposing service check(s)", tg.Name) 140 } 141 } 142 return nil 143 } 144 145 // tgUsesExposeCheck returns true if any group service in the task group makes 146 // use of the expose field. 147 func tgUsesExposeCheck(tg *structs.TaskGroup) bool { 148 for _, s := range tg.Services { 149 for _, check := range s.Checks { 150 if check.Expose { 151 return true 152 } 153 } 154 } 155 return false 156 } 157 158 // serviceUsesConnectEnvoy returns true if the service is going to end up using 159 // the built-in envoy proxy. 160 // 161 // This implementation is kind of reading tea leaves - firstly Connect 162 // must be enabled, and second the sidecar_task must not be overridden. If these 163 // conditions are met, the preceding connect hook will have injected a Connect 164 // sidecar task, the configuration of which is interpolated at runtime. 165 func serviceUsesConnectEnvoy(s *structs.Service) bool { 166 // A non-nil connect stanza implies this service isn't connect enabled in 167 // the first place. 168 if s.Connect == nil { 169 return false 170 } 171 172 // A non-nil connect.sidecar_task stanza implies the sidecar task is being 173 // overridden (i.e. the default Envoy is not being uesd). 174 if s.Connect.SidecarTask != nil { 175 return false 176 } 177 178 return true 179 } 180 181 // checkIsExposable returns true if check is qualified for automatic generation 182 // of connect proxy expose path configuration based on configured consul checks. 183 // To qualify, the check must be of type "http" or "grpc", and must have a Path 184 // configured. 185 func checkIsExposable(check *structs.ServiceCheck) bool { 186 switch strings.ToLower(check.Type) { 187 case "grpc", "http": 188 return strings.HasPrefix(check.Path, "/") 189 default: 190 return false 191 } 192 } 193 194 // exposePathForCheck extrapolates the necessary expose path configuration for 195 // the given consul service check. If the check is not compatible, nil is 196 // returned. 197 func exposePathForCheck(tg *structs.TaskGroup, s *structs.Service, check *structs.ServiceCheck) (*structs.ConsulExposePath, error) { 198 if !checkIsExposable(check) { 199 return nil, nil 200 } 201 202 // If the check is exposable but doesn't have a port label set build 203 // a port with a generated label, add it to the group's Dynamic ports 204 // and set the check port label to the generated label. 205 // 206 // This lets PortLabel be optional for any exposed check. 207 if check.PortLabel == "" { 208 port := structs.Port{ 209 Label: fmt.Sprintf("svc_%s_ck_%s", s.Name, uuid.Generate()[:6]), 210 To: -1, 211 } 212 213 tg.Networks[0].DynamicPorts = append(tg.Networks[0].DynamicPorts, port) 214 check.PortLabel = port.Label 215 } 216 217 // Determine the local service port (i.e. what port the service is actually 218 // listening to inside the network namespace). 219 // 220 // Similar logic exists in getAddress of client.go which is used for 221 // creating check & service registration objects. 222 // 223 // The difference here is the address is predestined to be localhost since 224 // it is binding inside the namespace. 225 var port int 226 if _, port = tg.Networks.Port(s.PortLabel); port <= 0 { // try looking up by port label 227 if port, _ = strconv.Atoi(s.PortLabel); port <= 0 { // then try direct port value 228 return nil, errors.Errorf( 229 "unable to determine local service port for service check %s->%s->%s", 230 tg.Name, s.Name, check.Name, 231 ) 232 } 233 } 234 235 // The Path, Protocol, and PortLabel are just copied over from the service 236 // check definition. 237 return &structs.ConsulExposePath{ 238 Path: check.Path, 239 Protocol: check.Protocol, 240 LocalPathPort: port, 241 ListenerPort: check.PortLabel, 242 }, nil 243 }