open-nomad/jobspec/parse_service.go
Seth Hoenig 5b072029f2 consul/connect: add initial support for ingress gateways
This PR adds initial support for running Consul Connect Ingress Gateways (CIGs) in Nomad. These gateways are declared as part of a task group level service definition within the connect stanza.

```hcl
service {
  connect {
    gateway {
      proxy {
        // envoy proxy configuration
      }
      ingress {
        // ingress-gateway configuration entry
      }
    }
  }
}
```

A gateway can be run in `bridge` or `host` networking mode, with the caveat that host networking necessitates manually specifying the Envoy admin listener (which cannot be disabled) via the service port value.

Currently Envoy is the only supported gateway implementation in Consul, and Nomad only supports running Envoy as a gateway using the docker driver.

Aims to address #8294 and tangentially #8647
2020-08-21 16:21:54 -05:00

972 lines
22 KiB
Go

package jobspec
import (
"fmt"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/helper"
"github.com/mitchellh/mapstructure"
)
func parseGroupServices(g *api.TaskGroup, serviceObjs *ast.ObjectList) error {
g.Services = make([]*api.Service, len(serviceObjs.Items))
for idx, o := range serviceObjs.Items {
service, err := parseService(o)
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("service (%d):", idx))
}
g.Services[idx] = service
}
return nil
}
func parseServices(serviceObjs *ast.ObjectList) ([]*api.Service, error) {
services := make([]*api.Service, len(serviceObjs.Items))
for idx, o := range serviceObjs.Items {
service, err := parseService(o)
if err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("service (%d):", idx))
}
services[idx] = service
}
return services, nil
}
func parseService(o *ast.ObjectItem) (*api.Service, error) {
// Check for invalid keys
valid := []string{
"name",
"tags",
"canary_tags",
"enable_tag_override",
"port",
"check",
"address_mode",
"check_restart",
"connect",
"task",
"meta",
"canary_meta",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, err
}
var service api.Service
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "check")
delete(m, "check_restart")
delete(m, "connect")
delete(m, "meta")
delete(m, "canary_meta")
if err := mapstructure.WeakDecode(m, &service); err != nil {
return nil, err
}
// Filter list
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("'%s': should be an object", service.Name)
}
if co := listVal.Filter("check"); len(co.Items) > 0 {
if err := parseChecks(&service, co); err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("'%s',", service.Name))
}
}
// Filter check_restart
if cro := listVal.Filter("check_restart"); len(cro.Items) > 0 {
if len(cro.Items) > 1 {
return nil, fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", service.Name)
}
cr, err := parseCheckRestart(cro.Items[0])
if err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("'%s',", service.Name))
}
service.CheckRestart = cr
}
// Filter connect
if co := listVal.Filter("connect"); len(co.Items) > 0 {
if len(co.Items) > 1 {
return nil, fmt.Errorf("connect '%s': cannot have more than 1 connect stanza", service.Name)
}
c, err := parseConnect(co.Items[0])
if err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("'%s',", service.Name))
}
service.Connect = c
}
// Parse out meta fields. These are in HCL as a list so we need
// to iterate over them and merge them.
if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 {
for _, o := range metaO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
if err := mapstructure.WeakDecode(m, &service.Meta); err != nil {
return nil, err
}
}
}
// Parse out canary_meta fields. These are in HCL as a list so we need
// to iterate over them and merge them.
if metaO := listVal.Filter("canary_meta"); len(metaO.Items) > 0 {
for _, o := range metaO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
if err := mapstructure.WeakDecode(m, &service.CanaryMeta); err != nil {
return nil, err
}
}
}
return &service, nil
}
func parseConnect(co *ast.ObjectItem) (*api.ConsulConnect, error) {
valid := []string{
"native",
"gateway",
"sidecar_service",
"sidecar_task",
}
if err := helper.CheckHCLKeys(co.Val, valid); err != nil {
return nil, multierror.Prefix(err, "connect ->")
}
var connect api.ConsulConnect
var m map[string]interface{}
if err := hcl.DecodeObject(&m, co.Val); err != nil {
return nil, err
}
delete(m, "gateway")
delete(m, "sidecar_service")
delete(m, "sidecar_task")
if err := mapstructure.WeakDecode(m, &connect); err != nil {
return nil, err
}
var connectList *ast.ObjectList
if ot, ok := co.Val.(*ast.ObjectType); ok {
connectList = ot.List
} else {
return nil, fmt.Errorf("connect should be an object")
}
// Parse the gateway
o := connectList.Filter("gateway")
if len(o.Items) > 1 {
return nil, fmt.Errorf("only one 'gateway' block allowed per task")
} else if len(o.Items) == 1 {
g, err := parseGateway(o.Items[0])
if err != nil {
return nil, fmt.Errorf("gateway, %v", err)
}
connect.Gateway = g
}
// Parse the sidecar_service
o = connectList.Filter("sidecar_service")
if len(o.Items) == 0 {
return &connect, nil
}
if len(o.Items) > 1 {
return nil, fmt.Errorf("only one 'sidecar_service' block allowed per task")
}
r, err := parseSidecarService(o.Items[0])
if err != nil {
return nil, fmt.Errorf("sidecar_service, %v", err)
}
connect.SidecarService = r
// Parse the sidecar_task
o = connectList.Filter("sidecar_task")
if len(o.Items) == 0 {
return &connect, nil
}
if len(o.Items) > 1 {
return nil, fmt.Errorf("only one 'sidecar_task' block allowed per task")
}
t, err := parseSidecarTask(o.Items[0])
if err != nil {
return nil, fmt.Errorf("sidecar_task, %v", err)
}
connect.SidecarTask = t
return &connect, nil
}
func parseGateway(o *ast.ObjectItem) (*api.ConsulGateway, error) {
valid := []string{
"proxy",
"ingress",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "gateway ->")
}
var gateway api.ConsulGateway
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "proxy")
delete(m, "ingress")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &gateway,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, fmt.Errorf("gateway: %v", err)
}
// list of parameters
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("proxy: should be an object")
}
// extract and parse the proxy block
po := listVal.Filter("proxy")
if len(po.Items) != 1 {
return nil, fmt.Errorf("must have one 'proxy' block")
}
proxy, err := parseGatewayProxy(po.Items[0])
if err != nil {
return nil, fmt.Errorf("proxy, %v", err)
}
gateway.Proxy = proxy
// extract and parse the ingress block
io := listVal.Filter("ingress")
if len(io.Items) != 1 {
// in the future, may be terminating or mesh block instead
return nil, fmt.Errorf("must have one 'ingress' block")
}
ingress, err := parseIngressConfigEntry(io.Items[0])
if err != nil {
return nil, fmt.Errorf("ingress, %v", err)
}
gateway.Ingress = ingress
return &gateway, nil
}
// parseGatewayProxy parses envoy gateway proxy options supported by Consul.
//
// consul.io/docs/connect/proxies/envoy#gateway-options
func parseGatewayProxy(o *ast.ObjectItem) (*api.ConsulGatewayProxy, error) {
valid := []string{
"connect_timeout",
"envoy_gateway_bind_tagged_addresses",
"envoy_gateway_bind_addresses",
"envoy_gateway_no_default_bind",
"envoy_dns_discovery_type",
"config",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "proxy ->")
}
var proxy api.ConsulGatewayProxy
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "config")
delete(m, "envoy_gateway_bind_addresses")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &proxy,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, fmt.Errorf("proxy: %v", err)
}
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("proxy: should be an object")
}
// need to parse envoy_gateway_bind_addresses if present
if ebo := listVal.Filter("envoy_gateway_bind_addresses"); len(ebo.Items) > 0 {
proxy.EnvoyGatewayBindAddresses = make(map[string]*api.ConsulGatewayBindAddress)
for _, listenerM := range ebo.Items { // object item, each listener object
listenerName := listenerM.Keys[0].Token.Value().(string)
var listenerListVal *ast.ObjectList
if ot, ok := listenerM.Val.(*ast.ObjectType); ok {
listenerListVal = ot.List
} else {
return nil, fmt.Errorf("listener: should be an object")
}
var bind api.ConsulGatewayBindAddress
if err := hcl.DecodeObject(&bind, listenerListVal); err != nil {
panic(err)
}
proxy.EnvoyGatewayBindAddresses[listenerName] = &bind
}
}
// need to parse the opaque config if present
if co := listVal.Filter("config"); len(co.Items) > 1 {
return nil, fmt.Errorf("only 1 meta object supported")
} else if len(co.Items) == 1 {
var mSlice []map[string]interface{}
if err := hcl.DecodeObject(&mSlice, co.Items[0].Val); err != nil {
return nil, err
}
if len(mSlice) > 1 {
return nil, fmt.Errorf("only 1 meta object supported")
}
m := mSlice[0]
if err := mapstructure.WeakDecode(m, &proxy.Config); err != nil {
return nil, err
}
proxy.Config = flattenMapSlice(proxy.Config)
}
return &proxy, nil
}
func parseConsulIngressService(o *ast.ObjectItem) (*api.ConsulIngressService, error) {
valid := []string{
"name",
"hosts",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "service ->")
}
var service api.ConsulIngressService
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &service,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return &service, nil
}
func parseConsulIngressListener(o *ast.ObjectItem) (*api.ConsulIngressListener, error) {
valid := []string{
"port",
"protocol",
"service",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "listener ->")
}
var listener api.ConsulIngressListener
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "service")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &listener,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
// Parse services
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("listener: should be an object")
}
so := listVal.Filter("service")
if len(so.Items) > 0 {
listener.Services = make([]*api.ConsulIngressService, len(so.Items))
for i := range so.Items {
is, err := parseConsulIngressService(so.Items[i])
if err != nil {
return nil, err
}
listener.Services[i] = is
}
}
return &listener, nil
}
func parseConsulGatewayTLS(o *ast.ObjectItem) (*api.ConsulGatewayTLSConfig, error) {
valid := []string{
"enabled",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "tls ->")
}
var tls api.ConsulGatewayTLSConfig
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &tls,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return &tls, nil
}
func parseIngressConfigEntry(o *ast.ObjectItem) (*api.ConsulIngressConfigEntry, error) {
valid := []string{
"tls",
"listener",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "ingress ->")
}
var ingress api.ConsulIngressConfigEntry
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "tls")
delete(m, "listener")
// Parse tls and listener(s)
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("ingress: should be an object")
}
if to := listVal.Filter("tls"); len(to.Items) > 1 {
return nil, fmt.Errorf("only 1 tls object supported")
} else if len(to.Items) == 1 {
if tls, err := parseConsulGatewayTLS(to.Items[0]); err != nil {
return nil, err
} else {
ingress.TLS = tls
}
}
lo := listVal.Filter("listener")
if len(lo.Items) > 0 {
ingress.Listeners = make([]*api.ConsulIngressListener, len(lo.Items))
for i := range lo.Items {
listener, err := parseConsulIngressListener(lo.Items[i])
if err != nil {
return nil, err
}
ingress.Listeners[i] = listener
}
}
return &ingress, nil
}
func parseSidecarService(o *ast.ObjectItem) (*api.ConsulSidecarService, error) {
valid := []string{
"port",
"proxy",
"tags",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "sidecar_service ->")
}
var sidecar api.ConsulSidecarService
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "proxy")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &sidecar,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, fmt.Errorf("sidecar_service: %v", err)
}
var proxyList *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
proxyList = ot.List
} else {
return nil, fmt.Errorf("sidecar_service: should be an object")
}
// Parse the proxy
po := proxyList.Filter("proxy")
if len(po.Items) == 0 {
return &sidecar, nil
}
if len(po.Items) > 1 {
return nil, fmt.Errorf("only one 'proxy' block allowed per task")
}
r, err := parseProxy(po.Items[0])
if err != nil {
return nil, fmt.Errorf("proxy, %v", err)
}
sidecar.Proxy = r
return &sidecar, nil
}
func parseSidecarTask(item *ast.ObjectItem) (*api.SidecarTask, error) {
task, err := parseTask(item, sidecarTaskKeys)
if err != nil {
return nil, err
}
sidecarTask := &api.SidecarTask{
Name: task.Name,
Driver: task.Driver,
User: task.User,
Config: task.Config,
Env: task.Env,
Resources: task.Resources,
Meta: task.Meta,
KillTimeout: task.KillTimeout,
LogConfig: task.LogConfig,
KillSignal: task.KillSignal,
}
// Parse ShutdownDelay separatly to get pointer
var m map[string]interface{}
if err := hcl.DecodeObject(&m, item.Val); err != nil {
return nil, err
}
m = map[string]interface{}{
"shutdown_delay": m["shutdown_delay"],
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: sidecarTask,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return sidecarTask, nil
}
func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) {
valid := []string{
"local_service_address",
"local_service_port",
"upstreams",
"expose",
"config",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return nil, multierror.Prefix(err, "proxy ->")
}
var proxy api.ConsulProxy
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return nil, err
}
delete(m, "upstreams")
delete(m, "expose")
delete(m, "config")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &proxy,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, fmt.Errorf("proxy: %v", err)
}
// Parse upstreams, expose, and config
var listVal *ast.ObjectList
if ot, ok := o.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("proxy: should be an object")
}
uo := listVal.Filter("upstreams")
if len(uo.Items) > 0 {
proxy.Upstreams = make([]*api.ConsulUpstream, len(uo.Items))
for i := range uo.Items {
u, err := parseUpstream(uo.Items[i])
if err != nil {
return nil, err
}
proxy.Upstreams[i] = u
}
}
if eo := listVal.Filter("expose"); len(eo.Items) > 1 {
return nil, fmt.Errorf("only 1 expose object supported")
} else if len(eo.Items) == 1 {
if e, err := parseExpose(eo.Items[0]); err != nil {
return nil, err
} else {
proxy.ExposeConfig = e
}
}
// If we have config, then parse that
if o := listVal.Filter("config"); len(o.Items) > 1 {
return nil, fmt.Errorf("only 1 meta object supported")
} else if len(o.Items) == 1 {
var mSlice []map[string]interface{}
if err := hcl.DecodeObject(&mSlice, o.Items[0].Val); err != nil {
return nil, err
}
if len(mSlice) > 1 {
return nil, fmt.Errorf("only 1 meta object supported")
}
m := mSlice[0]
if err := mapstructure.WeakDecode(m, &proxy.Config); err != nil {
return nil, err
}
proxy.Config = flattenMapSlice(proxy.Config)
}
return &proxy, nil
}
func parseExpose(eo *ast.ObjectItem) (*api.ConsulExposeConfig, error) {
valid := []string{
"path", // an array of path blocks
}
if err := helper.CheckHCLKeys(eo.Val, valid); err != nil {
return nil, multierror.Prefix(err, "expose ->")
}
var expose api.ConsulExposeConfig
var listVal *ast.ObjectList
if eoType, ok := eo.Val.(*ast.ObjectType); ok {
listVal = eoType.List
} else {
return nil, fmt.Errorf("expose: should be an object")
}
// Parse the expose block
po := listVal.Filter("path") // array
if len(po.Items) > 0 {
expose.Path = make([]*api.ConsulExposePath, len(po.Items))
for i := range po.Items {
p, err := parseExposePath(po.Items[i])
if err != nil {
return nil, err
}
expose.Path[i] = p
}
}
return &expose, nil
}
func parseExposePath(epo *ast.ObjectItem) (*api.ConsulExposePath, error) {
valid := []string{
"path",
"protocol",
"local_path_port",
"listener_port",
}
if err := helper.CheckHCLKeys(epo.Val, valid); err != nil {
return nil, multierror.Prefix(err, "path ->")
}
var path api.ConsulExposePath
var m map[string]interface{}
if err := hcl.DecodeObject(&m, epo.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &path,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return &path, nil
}
func parseUpstream(uo *ast.ObjectItem) (*api.ConsulUpstream, error) {
valid := []string{
"destination_name",
"local_bind_port",
}
if err := helper.CheckHCLKeys(uo.Val, valid); err != nil {
return nil, multierror.Prefix(err, "upstream ->")
}
var upstream api.ConsulUpstream
var m map[string]interface{}
if err := hcl.DecodeObject(&m, uo.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &upstream,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return &upstream, nil
}
func parseChecks(service *api.Service, checkObjs *ast.ObjectList) error {
service.Checks = make([]api.ServiceCheck, len(checkObjs.Items))
for idx, co := range checkObjs.Items {
// Check for invalid keys
valid := []string{
"name",
"type",
"interval",
"timeout",
"path",
"protocol",
"port",
"expose",
"command",
"args",
"initial_status",
"tls_skip_verify",
"header",
"method",
"check_restart",
"address_mode",
"grpc_service",
"grpc_use_tls",
"task",
"success_before_passing",
"failures_before_critical",
}
if err := helper.CheckHCLKeys(co.Val, valid); err != nil {
return multierror.Prefix(err, "check ->")
}
var check api.ServiceCheck
var cm map[string]interface{}
if err := hcl.DecodeObject(&cm, co.Val); err != nil {
return err
}
// HCL allows repeating stanzas so merge 'header' into a single
// map[string][]string.
if headerI, ok := cm["header"]; ok {
headerRaw, ok := headerI.([]map[string]interface{})
if !ok {
return fmt.Errorf("check -> header -> expected a []map[string][]string but found %T", headerI)
}
m := map[string][]string{}
for _, rawm := range headerRaw {
for k, vI := range rawm {
vs, ok := vI.([]interface{})
if !ok {
return fmt.Errorf("check -> header -> %q expected a []string but found %T", k, vI)
}
for _, vI := range vs {
v, ok := vI.(string)
if !ok {
return fmt.Errorf("check -> header -> %q expected a string but found %T", k, vI)
}
m[k] = append(m[k], v)
}
}
}
check.Header = m
// Remove "header" as it has been parsed
delete(cm, "header")
}
delete(cm, "check_restart")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &check,
})
if err != nil {
return err
}
if err := dec.Decode(cm); err != nil {
return err
}
// Filter check_restart
var checkRestartList *ast.ObjectList
if ot, ok := co.Val.(*ast.ObjectType); ok {
checkRestartList = ot.List
} else {
return fmt.Errorf("check_restart '%s': should be an object", check.Name)
}
if cro := checkRestartList.Filter("check_restart"); len(cro.Items) > 0 {
if len(cro.Items) > 1 {
return fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", check.Name)
}
cr, err := parseCheckRestart(cro.Items[0])
if err != nil {
return multierror.Prefix(err, fmt.Sprintf("check: '%s',", check.Name))
}
check.CheckRestart = cr
}
service.Checks[idx] = check
}
return nil
}
func parseCheckRestart(cro *ast.ObjectItem) (*api.CheckRestart, error) {
valid := []string{
"limit",
"grace",
"ignore_warnings",
}
if err := helper.CheckHCLKeys(cro.Val, valid); err != nil {
return nil, multierror.Prefix(err, "check_restart ->")
}
var checkRestart api.CheckRestart
var crm map[string]interface{}
if err := hcl.DecodeObject(&crm, cro.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
Result: &checkRestart,
})
if err != nil {
return nil, err
}
if err := dec.Decode(crm); err != nil {
return nil, err
}
return &checkRestart, nil
}