client: Add option to enable hairpinMode on Nomad bridge (#15961)

* Add `bridge_network_hairpin_mode` client config setting
* Add node attribute: `nomad.bridge.hairpin_mode`
* Changed format string to use `%q` to escape user provided data
* Add test to validate template JSON for developer safety

Co-authored-by: Daniel Bennett <dbennett@hashicorp.com>
This commit is contained in:
Charlie Voiselle 2023-02-02 10:12:15 -05:00 committed by GitHub
parent 37834dffda
commit 4caac1a92f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 184 additions and 100 deletions

3
.changelog/15961.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
client: Add option to enable hairpinMode on Nomad bridge
```

View File

@ -524,12 +524,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartAll(ev) return ar.RestartAll(ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "prestart-oneshot": {State: "dead", Restarts: 1},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "poststart-oneshot": {State: "dead", Restarts: 1},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -538,12 +538,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartRunning(ev) return ar.RestartRunning(ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -561,12 +561,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartAll(ev) return ar.RestartAll(ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "prestart-oneshot": {State: "dead", Restarts: 1},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "poststart-oneshot": {State: "dead", Restarts: 1},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -584,12 +584,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartRunning(ev) return ar.RestartRunning(ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -599,12 +599,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartAll(ev) return ar.RestartAll(ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "prestart-oneshot": {State: "dead", Restarts: 1},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 1}, "poststart-oneshot": {State: "dead", Restarts: 1},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -616,12 +616,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return nil return nil
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 0}, "main": {State: "dead", Restarts: 0},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -630,12 +630,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartTask("main", ev) return ar.RestartTask("main", ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "prestart-sidecar": {State: "running", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "poststart-sidecar": {State: "running", Restarts: 0},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -645,12 +645,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartTask("main", ev) return ar.RestartTask("main", ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 1}, "main": {State: "running", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "prestart-sidecar": {State: "running", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "poststart-sidecar": {State: "running", Restarts: 0},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -668,12 +668,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return nil return nil
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 1}, "main": {State: "dead", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -692,12 +692,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return nil return nil
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 1}, "main": {State: "dead", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -715,12 +715,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return nil return nil
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 1}, "main": {State: "dead", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -738,12 +738,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return nil return nil
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 1}, "main": {State: "dead", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -764,12 +764,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
}, },
expectedErr: "Task not running", expectedErr: "Task not running",
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "dead", Restarts: 1}, "main": {State: "dead", Restarts: 1},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "prestart-sidecar": {State: "dead", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "dead", Restarts: 0}, "poststart-sidecar": {State: "dead", Restarts: 0},
"poststop": structs.TaskState{State: "dead", Restarts: 0}, "poststop": {State: "dead", Restarts: 0},
}, },
}, },
{ {
@ -778,12 +778,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartTask("prestart-sidecar", ev) return ar.RestartTask("prestart-sidecar", ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 0}, "main": {State: "running", Restarts: 0},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "prestart-sidecar": {State: "running", Restarts: 1},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "poststart-sidecar": {State: "running", Restarts: 0},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
{ {
@ -792,12 +792,12 @@ func TestAllocRunner_Lifecycle_Restart(t *testing.T) {
return ar.RestartTask("poststart-sidecar", ev) return ar.RestartTask("poststart-sidecar", ev)
}, },
expectedAfter: map[string]structs.TaskState{ expectedAfter: map[string]structs.TaskState{
"main": structs.TaskState{State: "running", Restarts: 0}, "main": {State: "running", Restarts: 0},
"prestart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "prestart-oneshot": {State: "dead", Restarts: 0},
"prestart-sidecar": structs.TaskState{State: "running", Restarts: 0}, "prestart-sidecar": {State: "running", Restarts: 0},
"poststart-oneshot": structs.TaskState{State: "dead", Restarts: 0}, "poststart-oneshot": {State: "dead", Restarts: 0},
"poststart-sidecar": structs.TaskState{State: "running", Restarts: 1}, "poststart-sidecar": {State: "running", Restarts: 1},
"poststop": structs.TaskState{State: "pending", Restarts: 0}, "poststop": {State: "pending", Restarts: 0},
}, },
}, },
} }

View File

@ -184,7 +184,7 @@ func newNetworkConfigurator(log hclog.Logger, alloc *structs.Allocation, config
switch { switch {
case netMode == "bridge": case netMode == "bridge":
c, err := newBridgeNetworkConfigurator(log, config.BridgeNetworkName, config.BridgeNetworkAllocSubnet, config.CNIPath, ignorePortMappingHostIP) c, err := newBridgeNetworkConfigurator(log, config.BridgeNetworkName, config.BridgeNetworkAllocSubnet, config.BridgeNetworkHairpinMode, config.CNIPath, ignorePortMappingHostIP)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,6 +3,7 @@ package allocrunner
import ( import (
"context" "context"
"fmt" "fmt"
"text/template"
"github.com/coreos/go-iptables/iptables" "github.com/coreos/go-iptables/iptables"
hclog "github.com/hashicorp/go-hclog" hclog "github.com/hashicorp/go-hclog"
@ -28,6 +29,8 @@ const (
cniAdminChainName = "NOMAD-ADMIN" cniAdminChainName = "NOMAD-ADMIN"
) )
var nomadBridgeTmpl = template.Must(template.New("cniConf").Parse(nomadCNIConfigTemplate))
// bridgeNetworkConfigurator is a NetworkConfigurator which adds the alloc to a // bridgeNetworkConfigurator is a NetworkConfigurator which adds the alloc to a
// shared bridge, configures masquerading for egress traffic and port mapping // shared bridge, configures masquerading for egress traffic and port mapping
// for ingress // for ingress
@ -35,14 +38,16 @@ type bridgeNetworkConfigurator struct {
cni *cniNetworkConfigurator cni *cniNetworkConfigurator
allocSubnet string allocSubnet string
bridgeName string bridgeName string
hairpinMode bool
logger hclog.Logger logger hclog.Logger
} }
func newBridgeNetworkConfigurator(log hclog.Logger, bridgeName, ipRange, cniPath string, ignorePortMappingHostIP bool) (*bridgeNetworkConfigurator, error) { func newBridgeNetworkConfigurator(log hclog.Logger, bridgeName, ipRange string, hairpinMode bool, cniPath string, ignorePortMappingHostIP bool) (*bridgeNetworkConfigurator, error) {
b := &bridgeNetworkConfigurator{ b := &bridgeNetworkConfigurator{
bridgeName: bridgeName, bridgeName: bridgeName,
allocSubnet: ipRange, allocSubnet: ipRange,
hairpinMode: hairpinMode,
logger: log, logger: log,
} }
@ -54,7 +59,7 @@ func newBridgeNetworkConfigurator(log hclog.Logger, bridgeName, ipRange, cniPath
b.allocSubnet = defaultNomadAllocSubnet b.allocSubnet = defaultNomadAllocSubnet
} }
c, err := newCNINetworkConfiguratorWithConf(log, cniPath, bridgeNetworkAllocIfPrefix, ignorePortMappingHostIP, buildNomadBridgeNetConfig(b.bridgeName, b.allocSubnet)) c, err := newCNINetworkConfiguratorWithConf(log, cniPath, bridgeNetworkAllocIfPrefix, ignorePortMappingHostIP, buildNomadBridgeNetConfig(*b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -134,8 +139,12 @@ func (b *bridgeNetworkConfigurator) Teardown(ctx context.Context, alloc *structs
return b.cni.Teardown(ctx, alloc, spec) return b.cni.Teardown(ctx, alloc, spec)
} }
func buildNomadBridgeNetConfig(bridgeName, subnet string) []byte { func buildNomadBridgeNetConfig(b bridgeNetworkConfigurator) []byte {
return []byte(fmt.Sprintf(nomadCNIConfigTemplate, bridgeName, subnet, cniAdminChainName)) return []byte(fmt.Sprintf(nomadCNIConfigTemplate,
b.bridgeName,
b.hairpinMode,
b.allocSubnet,
cniAdminChainName))
} }
const nomadCNIConfigTemplate = `{ const nomadCNIConfigTemplate = `{
@ -147,16 +156,17 @@ const nomadCNIConfigTemplate = `{
}, },
{ {
"type": "bridge", "type": "bridge",
"bridge": "%s", "bridge": %q,
"ipMasq": true, "ipMasq": true,
"isGateway": true, "isGateway": true,
"forceAddress": true, "forceAddress": true,
"hairpinMode": %v,
"ipam": { "ipam": {
"type": "host-local", "type": "host-local",
"ranges": [ "ranges": [
[ [
{ {
"subnet": "%s" "subnet": %q
} }
] ]
], ],
@ -168,7 +178,7 @@ const nomadCNIConfigTemplate = `{
{ {
"type": "firewall", "type": "firewall",
"backend": "iptables", "backend": "iptables",
"iptablesAdminChainName": "%s" "iptablesAdminChainName": %q
}, },
{ {
"type": "portmap", "type": "portmap",

View File

@ -0,0 +1,48 @@
package allocrunner
import (
"encoding/json"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
)
func Test_buildNomadBridgeNetConfig(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
b *bridgeNetworkConfigurator
}{
{
name: "empty",
b: &bridgeNetworkConfigurator{},
},
{
name: "hairpin",
b: &bridgeNetworkConfigurator{
bridgeName: defaultNomadBridgeName,
allocSubnet: defaultNomadAllocSubnet,
hairpinMode: true,
},
},
{
name: "bad_input",
b: &bridgeNetworkConfigurator{
bridgeName: `bad"`,
allocSubnet: defaultNomadAllocSubnet,
hairpinMode: true,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
ci.Parallel(t)
bCfg := buildNomadBridgeNetConfig(*tc.b)
// Validate that the JSON created is rational
must.True(t, json.Valid(bCfg))
})
}
}

View File

@ -261,6 +261,10 @@ type Config struct {
// networking mode. This defaults to 'nomad' if not set // networking mode. This defaults to 'nomad' if not set
BridgeNetworkName string BridgeNetworkName string
// BridgeNetworkHairpinMode is whether or not to enable hairpin mode on the
// internal bridge network
BridgeNetworkHairpinMode bool
// BridgeNetworkAllocSubnet is the IP subnet to use for address allocation // BridgeNetworkAllocSubnet is the IP subnet to use for address allocation
// for allocations in bridge networking mode. Subnet must be in CIDR // for allocations in bridge networking mode. Subnet must be in CIDR
// notation // notation

View File

@ -1,5 +1,4 @@
//go:build !linux //go:build !linux
// +build !linux
package fingerprint package fingerprint

View File

@ -1,3 +1,5 @@
//go:build linux
package fingerprint package fingerprint
import ( import (
@ -5,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
@ -35,6 +38,9 @@ func (f *BridgeFingerprint) Fingerprint(req *FingerprintRequest, resp *Fingerpri
}}, }},
} }
resp.AddAttribute("nomad.bridge.hairpin_mode",
strconv.FormatBool(req.Config.BridgeNetworkHairpinMode))
resp.Detected = true resp.Detected = true
return nil return nil
} }

View File

@ -788,6 +788,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
conf.CNIConfigDir = agentConfig.Client.CNIConfigDir conf.CNIConfigDir = agentConfig.Client.CNIConfigDir
conf.BridgeNetworkName = agentConfig.Client.BridgeNetworkName conf.BridgeNetworkName = agentConfig.Client.BridgeNetworkName
conf.BridgeNetworkAllocSubnet = agentConfig.Client.BridgeNetworkSubnet conf.BridgeNetworkAllocSubnet = agentConfig.Client.BridgeNetworkSubnet
conf.BridgeNetworkHairpinMode = agentConfig.Client.BridgeNetworkHairpinMode
for _, hn := range agentConfig.Client.HostNetworks { for _, hn := range agentConfig.Client.HostNetworks {
conf.HostNetworks[hn.Name] = hn conf.HostNetworks[hn.Name] = hn

View File

@ -315,6 +315,10 @@ type ClientConfig struct {
// the host // the host
BridgeNetworkSubnet string `hcl:"bridge_network_subnet"` BridgeNetworkSubnet string `hcl:"bridge_network_subnet"`
// BridgeNetworkHairpinMode is whether or not to enable hairpin mode on the
// internal bridge network
BridgeNetworkHairpinMode bool `hcl:"bridge_network_hairpin_mode"`
// HostNetworks describes the different host networks available to the host // HostNetworks describes the different host networks available to the host
// if the host uses multiple interfaces // if the host uses multiple interfaces
HostNetworks []*structs.ClientHostNetworkConfig `hcl:"host_network"` HostNetworks []*structs.ClientHostNetworkConfig `hcl:"host_network"`
@ -2137,6 +2141,10 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
result.BridgeNetworkSubnet = b.BridgeNetworkSubnet result.BridgeNetworkSubnet = b.BridgeNetworkSubnet
} }
if b.BridgeNetworkHairpinMode {
result.BridgeNetworkHairpinMode = true
}
result.HostNetworks = a.HostNetworks result.HostNetworks = a.HostNetworks
if len(b.HostNetworks) != 0 { if len(b.HostNetworks) != 0 {

View File

@ -147,12 +147,17 @@ client {
configuration. configuration.
- `bridge_network_name` `(string: "nomad")` - Sets the name of the bridge to be - `bridge_network_name` `(string: "nomad")` - Sets the name of the bridge to be
created by nomad for allocations running with bridge networking mode on the created by Nomad for allocations running with bridge networking mode on the
client. client.
- `bridge_network_subnet` `(string: "172.26.64.0/20")` - Specifies the subnet - `bridge_network_subnet` `(string: "172.26.64.0/20")` - Specifies the subnet
which the client will use to allocate IP addresses from. which the client will use to allocate IP addresses from.
- `bridge_network_hairpin_mode` `(bool: false)` - Specifies if hairpin mode
is enabled on the network bridge created by Nomad for allocations running
with bridge networking mode on this client. You may use the corresponding
node attribute `nomad.bridge.hairpin_mode` in constraints.
- `artifact` <code>([Artifact](#artifact-parameters): varied)</code> - - `artifact` <code>([Artifact](#artifact-parameters): varied)</code> -
Specifies controls on the behavior of task Specifies controls on the behavior of task
[`artifact`](/nomad/docs/job-specification/artifact) blocks. [`artifact`](/nomad/docs/job-specification/artifact) blocks.