1package api
2
3import (
4	"fmt"
5	"time"
6)
7
8// CheckRestart describes if and when a task should be restarted based on
9// failing health checks.
10type CheckRestart struct {
11	Limit          int            `mapstructure:"limit" hcl:"limit,optional"`
12	Grace          *time.Duration `mapstructure:"grace" hcl:"grace,optional"`
13	IgnoreWarnings bool           `mapstructure:"ignore_warnings" hcl:"ignore_warnings,optional"`
14}
15
16// Canonicalize CheckRestart fields if not nil.
17func (c *CheckRestart) Canonicalize() {
18	if c == nil {
19		return
20	}
21
22	if c.Grace == nil {
23		c.Grace = timeToPtr(1 * time.Second)
24	}
25}
26
27// Copy returns a copy of CheckRestart or nil if unset.
28func (c *CheckRestart) Copy() *CheckRestart {
29	if c == nil {
30		return nil
31	}
32
33	nc := new(CheckRestart)
34	nc.Limit = c.Limit
35	if c.Grace != nil {
36		g := *c.Grace
37		nc.Grace = &g
38	}
39	nc.IgnoreWarnings = c.IgnoreWarnings
40	return nc
41}
42
43// Merge values from other CheckRestart over default values on this
44// CheckRestart and return merged copy.
45func (c *CheckRestart) Merge(o *CheckRestart) *CheckRestart {
46	if c == nil {
47		// Just return other
48		return o
49	}
50
51	nc := c.Copy()
52
53	if o == nil {
54		// Nothing to merge
55		return nc
56	}
57
58	if o.Limit > 0 {
59		nc.Limit = o.Limit
60	}
61
62	if o.Grace != nil {
63		nc.Grace = o.Grace
64	}
65
66	if o.IgnoreWarnings {
67		nc.IgnoreWarnings = o.IgnoreWarnings
68	}
69
70	return nc
71}
72
73// ServiceCheck represents the consul health check that Nomad registers.
74type ServiceCheck struct {
75	//FIXME Id is unused. Remove?
76	Id                     string              `hcl:"id,optional"`
77	Name                   string              `hcl:"name,optional"`
78	Type                   string              `hcl:"type,optional"`
79	Command                string              `hcl:"command,optional"`
80	Args                   []string            `hcl:"args,optional"`
81	Path                   string              `hcl:"path,optional"`
82	Protocol               string              `hcl:"protocol,optional"`
83	PortLabel              string              `mapstructure:"port" hcl:"port,optional"`
84	Expose                 bool                `hcl:"expose,optional"`
85	AddressMode            string              `mapstructure:"address_mode" hcl:"address_mode,optional"`
86	Interval               time.Duration       `hcl:"interval,optional"`
87	Timeout                time.Duration       `hcl:"timeout,optional"`
88	InitialStatus          string              `mapstructure:"initial_status" hcl:"initial_status,optional"`
89	TLSSkipVerify          bool                `mapstructure:"tls_skip_verify" hcl:"tls_skip_verify,optional"`
90	Header                 map[string][]string `hcl:"header,block"`
91	Method                 string              `hcl:"method,optional"`
92	CheckRestart           *CheckRestart       `mapstructure:"check_restart" hcl:"check_restart,block"`
93	GRPCService            string              `mapstructure:"grpc_service" hcl:"grpc_service,optional"`
94	GRPCUseTLS             bool                `mapstructure:"grpc_use_tls" hcl:"grpc_use_tls,optional"`
95	TaskName               string              `mapstructure:"task" hcl:"task,optional"`
96	SuccessBeforePassing   int                 `mapstructure:"success_before_passing" hcl:"success_before_passing,optional"`
97	FailuresBeforeCritical int                 `mapstructure:"failures_before_critical" hcl:"failures_before_critical,optional"`
98	Body                   string              `hcl:"body,optional"`
99	OnUpdate               string              `mapstructure:"on_update" hcl:"on_update,optional"`
100}
101
102// Service represents a Consul service definition.
103type Service struct {
104	//FIXME Id is unused. Remove?
105	Id                string            `hcl:"id,optional"`
106	Name              string            `hcl:"name,optional"`
107	Tags              []string          `hcl:"tags,optional"`
108	CanaryTags        []string          `mapstructure:"canary_tags" hcl:"canary_tags,optional"`
109	EnableTagOverride bool              `mapstructure:"enable_tag_override" hcl:"enable_tag_override,optional"`
110	PortLabel         string            `mapstructure:"port" hcl:"port,optional"`
111	AddressMode       string            `mapstructure:"address_mode" hcl:"address_mode,optional"`
112	Checks            []ServiceCheck    `hcl:"check,block"`
113	CheckRestart      *CheckRestart     `mapstructure:"check_restart" hcl:"check_restart,block"`
114	Connect           *ConsulConnect    `hcl:"connect,block"`
115	Meta              map[string]string `hcl:"meta,block"`
116	CanaryMeta        map[string]string `hcl:"canary_meta,block"`
117	TaskName          string            `mapstructure:"task" hcl:"task,optional"`
118	OnUpdate          string            `mapstructure:"on_update" hcl:"on_update,optional"`
119}
120
121const (
122	OnUpdateRequireHealthy = "require_healthy"
123	OnUpdateIgnoreWarn     = "ignore_warnings"
124	OnUpdateIgnore         = "ignore"
125)
126
127// Canonicalize the Service by ensuring its name and address mode are set. Task
128// will be nil for group services.
129func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) {
130	if s.Name == "" {
131		if t != nil {
132			s.Name = fmt.Sprintf("%s-%s-%s", *job.Name, *tg.Name, t.Name)
133		} else {
134			s.Name = fmt.Sprintf("%s-%s", *job.Name, *tg.Name)
135		}
136	}
137
138	// Default to AddressModeAuto
139	if s.AddressMode == "" {
140		s.AddressMode = "auto"
141	}
142
143	// Default to OnUpdateRequireHealthy
144	if s.OnUpdate == "" {
145		s.OnUpdate = OnUpdateRequireHealthy
146	}
147
148	s.Connect.Canonicalize()
149
150	// Canonicalize CheckRestart on Checks and merge Service.CheckRestart
151	// into each check.
152	for i, check := range s.Checks {
153		s.Checks[i].CheckRestart = s.CheckRestart.Merge(check.CheckRestart)
154		s.Checks[i].CheckRestart.Canonicalize()
155
156		if s.Checks[i].SuccessBeforePassing < 0 {
157			s.Checks[i].SuccessBeforePassing = 0
158		}
159
160		if s.Checks[i].FailuresBeforeCritical < 0 {
161			s.Checks[i].FailuresBeforeCritical = 0
162		}
163
164		// Inhert Service
165		if s.Checks[i].OnUpdate == "" {
166			s.Checks[i].OnUpdate = s.OnUpdate
167		}
168	}
169}
170
171// ConsulConnect represents a Consul Connect jobspec stanza.
172type ConsulConnect struct {
173	Native         bool                  `hcl:"native,optional"`
174	Gateway        *ConsulGateway        `hcl:"gateway,block"`
175	SidecarService *ConsulSidecarService `mapstructure:"sidecar_service" hcl:"sidecar_service,block"`
176	SidecarTask    *SidecarTask          `mapstructure:"sidecar_task" hcl:"sidecar_task,block"`
177}
178
179func (cc *ConsulConnect) Canonicalize() {
180	if cc == nil {
181		return
182	}
183
184	cc.SidecarService.Canonicalize()
185	cc.SidecarTask.Canonicalize()
186	cc.Gateway.Canonicalize()
187}
188
189// ConsulSidecarService represents a Consul Connect SidecarService jobspec
190// stanza.
191type ConsulSidecarService struct {
192	Tags                   []string     `hcl:"tags,optional"`
193	Port                   string       `hcl:"port,optional"`
194	Proxy                  *ConsulProxy `hcl:"proxy,block"`
195	DisableDefaultTCPCheck bool         `mapstructure:"disable_default_tcp_check" hcl:"disable_default_tcp_check,optional"`
196}
197
198func (css *ConsulSidecarService) Canonicalize() {
199	if css == nil {
200		return
201	}
202
203	if len(css.Tags) == 0 {
204		css.Tags = nil
205	}
206
207	css.Proxy.Canonicalize()
208}
209
210// SidecarTask represents a subset of Task fields that can be set to override
211// the fields of the Task generated for the sidecar
212type SidecarTask struct {
213	Name          string                 `hcl:"name,optional"`
214	Driver        string                 `hcl:"driver,optional"`
215	User          string                 `hcl:"user,optional"`
216	Config        map[string]interface{} `hcl:"config,block"`
217	Env           map[string]string      `hcl:"env,block"`
218	Resources     *Resources             `hcl:"resources,block"`
219	Meta          map[string]string      `hcl:"meta,block"`
220	KillTimeout   *time.Duration         `mapstructure:"kill_timeout" hcl:"kill_timeout,optional"`
221	LogConfig     *LogConfig             `mapstructure:"logs" hcl:"logs,block"`
222	ShutdownDelay *time.Duration         `mapstructure:"shutdown_delay" hcl:"shutdown_delay,optional"`
223	KillSignal    string                 `mapstructure:"kill_signal" hcl:"kill_signal,optional"`
224}
225
226func (st *SidecarTask) Canonicalize() {
227	if st == nil {
228		return
229	}
230
231	if len(st.Config) == 0 {
232		st.Config = nil
233	}
234
235	if len(st.Env) == 0 {
236		st.Env = nil
237	}
238
239	if st.Resources == nil {
240		st.Resources = DefaultResources()
241	} else {
242		st.Resources.Canonicalize()
243	}
244
245	if st.LogConfig == nil {
246		st.LogConfig = DefaultLogConfig()
247	} else {
248		st.LogConfig.Canonicalize()
249	}
250
251	if len(st.Meta) == 0 {
252		st.Meta = nil
253	}
254
255	if st.KillTimeout == nil {
256		st.KillTimeout = timeToPtr(5 * time.Second)
257	}
258
259	if st.ShutdownDelay == nil {
260		st.ShutdownDelay = timeToPtr(0)
261	}
262}
263
264// ConsulProxy represents a Consul Connect sidecar proxy jobspec stanza.
265type ConsulProxy struct {
266	LocalServiceAddress string                 `mapstructure:"local_service_address" hcl:"local_service_address,optional"`
267	LocalServicePort    int                    `mapstructure:"local_service_port" hcl:"local_service_port,optional"`
268	ExposeConfig        *ConsulExposeConfig    `mapstructure:"expose" hcl:"expose,block"`
269	Upstreams           []*ConsulUpstream      `hcl:"upstreams,block"`
270	Config              map[string]interface{} `hcl:"config,block"`
271}
272
273func (cp *ConsulProxy) Canonicalize() {
274	if cp == nil {
275		return
276	}
277
278	cp.ExposeConfig.Canonicalize()
279
280	if len(cp.Upstreams) == 0 {
281		cp.Upstreams = nil
282	}
283
284	if len(cp.Config) == 0 {
285		cp.Config = nil
286	}
287}
288
289// ConsulUpstream represents a Consul Connect upstream jobspec stanza.
290type ConsulUpstream struct {
291	DestinationName  string `mapstructure:"destination_name" hcl:"destination_name,optional"`
292	LocalBindPort    int    `mapstructure:"local_bind_port" hcl:"local_bind_port,optional"`
293	Datacenter       string `mapstructure:"datacenter" hcl:"datacenter,optional"`
294	LocalBindAddress string `mapstructure:"local_bind_address" hcl:"local_bind_address,optional"`
295}
296
297type ConsulExposeConfig struct {
298	Path []*ConsulExposePath `mapstructure:"path" hcl:"path,block"`
299}
300
301func (cec *ConsulExposeConfig) Canonicalize() {
302	if cec == nil {
303		return
304	}
305
306	if len(cec.Path) == 0 {
307		cec.Path = nil
308	}
309}
310
311type ConsulExposePath struct {
312	Path          string `hcl:"path,optional"`
313	Protocol      string `hcl:"protocol,optional"`
314	LocalPathPort int    `mapstructure:"local_path_port" hcl:"local_path_port,optional"`
315	ListenerPort  string `mapstructure:"listener_port" hcl:"listener_port,optional"`
316}
317
318// ConsulGateway is used to configure one of the Consul Connect Gateway types.
319type ConsulGateway struct {
320	// Proxy is used to configure the Envoy instance acting as the gateway.
321	Proxy *ConsulGatewayProxy `hcl:"proxy,block"`
322
323	// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
324	Ingress *ConsulIngressConfigEntry `hcl:"ingress,block"`
325
326	// Terminating represents the Consul Configuration Entry for a Terminating Gateway.
327	Terminating *ConsulTerminatingConfigEntry `hcl:"terminating,block"`
328
329	// Mesh is not yet supported.
330	// Mesh *ConsulMeshConfigEntry
331}
332
333func (g *ConsulGateway) Canonicalize() {
334	if g == nil {
335		return
336	}
337	g.Proxy.Canonicalize()
338	g.Ingress.Canonicalize()
339	g.Terminating.Canonicalize()
340}
341
342func (g *ConsulGateway) Copy() *ConsulGateway {
343	if g == nil {
344		return nil
345	}
346
347	return &ConsulGateway{
348		Proxy:       g.Proxy.Copy(),
349		Ingress:     g.Ingress.Copy(),
350		Terminating: g.Terminating.Copy(),
351	}
352}
353
354type ConsulGatewayBindAddress struct {
355	Name    string `hcl:",label"`
356	Address string `mapstructure:"address" hcl:"address,optional"`
357	Port    int    `mapstructure:"port" hcl:"port,optional"`
358}
359
360var (
361	// defaultGatewayConnectTimeout is the default amount of time connections to
362	// upstreams are allowed before timing out.
363	defaultGatewayConnectTimeout = 5 * time.Second
364)
365
366// ConsulGatewayProxy is used to tune parameters of the proxy instance acting as
367// one of the forms of Connect gateways that Consul supports.
368//
369// https://www.consul.io/docs/connect/proxies/envoy#gateway-options
370type ConsulGatewayProxy struct {
371	ConnectTimeout                  *time.Duration                       `mapstructure:"connect_timeout" hcl:"connect_timeout,optional"`
372	EnvoyGatewayBindTaggedAddresses bool                                 `mapstructure:"envoy_gateway_bind_tagged_addresses" hcl:"envoy_gateway_bind_tagged_addresses,optional"`
373	EnvoyGatewayBindAddresses       map[string]*ConsulGatewayBindAddress `mapstructure:"envoy_gateway_bind_addresses" hcl:"envoy_gateway_bind_addresses,block"`
374	EnvoyGatewayNoDefaultBind       bool                                 `mapstructure:"envoy_gateway_no_default_bind" hcl:"envoy_gateway_no_default_bind,optional"`
375	EnvoyDNSDiscoveryType           string                               `mapstructure:"envoy_dns_discovery_type" hcl:"envoy_dns_discovery_type,optional"`
376	Config                          map[string]interface{}               `hcl:"config,block"` // escape hatch envoy config
377}
378
379func (p *ConsulGatewayProxy) Canonicalize() {
380	if p == nil {
381		return
382	}
383
384	if p.ConnectTimeout == nil {
385		// same as the default from consul
386		p.ConnectTimeout = timeToPtr(defaultGatewayConnectTimeout)
387	}
388
389	if len(p.EnvoyGatewayBindAddresses) == 0 {
390		p.EnvoyGatewayBindAddresses = nil
391	}
392
393	if len(p.Config) == 0 {
394		p.Config = nil
395	}
396}
397
398func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
399	if p == nil {
400		return nil
401	}
402
403	var binds map[string]*ConsulGatewayBindAddress = nil
404	if p.EnvoyGatewayBindAddresses != nil {
405		binds = make(map[string]*ConsulGatewayBindAddress, len(p.EnvoyGatewayBindAddresses))
406		for k, v := range p.EnvoyGatewayBindAddresses {
407			binds[k] = v
408		}
409	}
410
411	var config map[string]interface{} = nil
412	if p.Config != nil {
413		config = make(map[string]interface{}, len(p.Config))
414		for k, v := range p.Config {
415			config[k] = v
416		}
417	}
418
419	return &ConsulGatewayProxy{
420		ConnectTimeout:                  timeToPtr(*p.ConnectTimeout),
421		EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
422		EnvoyGatewayBindAddresses:       binds,
423		EnvoyGatewayNoDefaultBind:       p.EnvoyGatewayNoDefaultBind,
424		EnvoyDNSDiscoveryType:           p.EnvoyDNSDiscoveryType,
425		Config:                          config,
426	}
427}
428
429// ConsulGatewayTLSConfig is used to configure TLS for a gateway.
430type ConsulGatewayTLSConfig struct {
431	Enabled bool `hcl:"enabled,optional"`
432}
433
434func (tc *ConsulGatewayTLSConfig) Canonicalize() {
435}
436
437func (tc *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig {
438	if tc == nil {
439		return nil
440	}
441
442	return &ConsulGatewayTLSConfig{
443		Enabled: tc.Enabled,
444	}
445}
446
447// ConsulIngressService is used to configure a service fronted by the ingress gateway.
448type ConsulIngressService struct {
449	// Namespace is not yet supported.
450	// Namespace string
451	Name string `hcl:"name,optional"`
452
453	Hosts []string `hcl:"hosts,optional"`
454}
455
456func (s *ConsulIngressService) Canonicalize() {
457	if s == nil {
458		return
459	}
460
461	if len(s.Hosts) == 0 {
462		s.Hosts = nil
463	}
464}
465
466func (s *ConsulIngressService) Copy() *ConsulIngressService {
467	if s == nil {
468		return nil
469	}
470
471	var hosts []string = nil
472	if n := len(s.Hosts); n > 0 {
473		hosts = make([]string, n)
474		copy(hosts, s.Hosts)
475	}
476
477	return &ConsulIngressService{
478		Name:  s.Name,
479		Hosts: hosts,
480	}
481}
482
483const (
484	defaultIngressListenerProtocol = "tcp"
485)
486
487// ConsulIngressListener is used to configure a listener on a Consul Ingress
488// Gateway.
489type ConsulIngressListener struct {
490	Port     int                     `hcl:"port,optional"`
491	Protocol string                  `hcl:"protocol,optional"`
492	Services []*ConsulIngressService `hcl:"service,block"`
493}
494
495func (l *ConsulIngressListener) Canonicalize() {
496	if l == nil {
497		return
498	}
499
500	if l.Protocol == "" {
501		// same as default from consul
502		l.Protocol = defaultIngressListenerProtocol
503	}
504
505	if len(l.Services) == 0 {
506		l.Services = nil
507	}
508}
509
510func (l *ConsulIngressListener) Copy() *ConsulIngressListener {
511	if l == nil {
512		return nil
513	}
514
515	var services []*ConsulIngressService = nil
516	if n := len(l.Services); n > 0 {
517		services = make([]*ConsulIngressService, n)
518		for i := 0; i < n; i++ {
519			services[i] = l.Services[i].Copy()
520		}
521	}
522
523	return &ConsulIngressListener{
524		Port:     l.Port,
525		Protocol: l.Protocol,
526		Services: services,
527	}
528}
529
530// ConsulIngressConfigEntry represents the Consul Configuration Entry type for
531// an Ingress Gateway.
532//
533// https://www.consul.io/docs/agent/config-entries/ingress-gateway#available-fields
534type ConsulIngressConfigEntry struct {
535	// Namespace is not yet supported.
536	// Namespace string
537
538	TLS       *ConsulGatewayTLSConfig  `hcl:"tls,block"`
539	Listeners []*ConsulIngressListener `hcl:"listener,block"`
540}
541
542func (e *ConsulIngressConfigEntry) Canonicalize() {
543	if e == nil {
544		return
545	}
546
547	e.TLS.Canonicalize()
548
549	if len(e.Listeners) == 0 {
550		e.Listeners = nil
551	}
552
553	for _, listener := range e.Listeners {
554		listener.Canonicalize()
555	}
556}
557
558func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
559	if e == nil {
560		return nil
561	}
562
563	var listeners []*ConsulIngressListener = nil
564	if n := len(e.Listeners); n > 0 {
565		listeners = make([]*ConsulIngressListener, n)
566		for i := 0; i < n; i++ {
567			listeners[i] = e.Listeners[i].Copy()
568		}
569	}
570
571	return &ConsulIngressConfigEntry{
572		TLS:       e.TLS.Copy(),
573		Listeners: listeners,
574	}
575}
576
577type ConsulLinkedService struct {
578	Name     string `hcl:"name,optional"`
579	CAFile   string `hcl:"ca_file,optional"`
580	CertFile string `hcl:"cert_file,optional"`
581	KeyFile  string `hcl:"key_file,optional"`
582	SNI      string `hcl:"sni,optional"`
583}
584
585func (s *ConsulLinkedService) Canonicalize() {
586	// nothing to do for now
587}
588
589func (s *ConsulLinkedService) Copy() *ConsulLinkedService {
590	if s == nil {
591		return nil
592	}
593
594	return &ConsulLinkedService{
595		Name:     s.Name,
596		CAFile:   s.CAFile,
597		CertFile: s.CertFile,
598		KeyFile:  s.KeyFile,
599		SNI:      s.SNI,
600	}
601}
602
603// ConsulTerminatingConfigEntry represents the Consul Configuration Entry type
604// for a Terminating Gateway.
605//
606// https://www.consul.io/docs/agent/config-entries/terminating-gateway#available-fields
607type ConsulTerminatingConfigEntry struct {
608	// Namespace is not yet supported.
609	// Namespace string
610
611	Services []*ConsulLinkedService `hcl:"service,block"`
612}
613
614func (e *ConsulTerminatingConfigEntry) Canonicalize() {
615	if e == nil {
616		return
617	}
618
619	if len(e.Services) == 0 {
620		e.Services = nil
621	}
622
623	for _, service := range e.Services {
624		service.Canonicalize()
625	}
626}
627
628func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry {
629	if e == nil {
630		return nil
631	}
632
633	var services []*ConsulLinkedService = nil
634	if n := len(e.Services); n > 0 {
635		services = make([]*ConsulLinkedService, n)
636		for i := 0; i < n; i++ {
637			services[i] = e.Services[i].Copy()
638		}
639	}
640
641	return &ConsulTerminatingConfigEntry{
642		Services: services,
643	}
644}
645
646// ConsulMeshConfigEntry is not yet supported.
647// type ConsulMeshConfigEntry struct {
648// }
649