open-nomad/client/allocrunner/network_hook.go
James Rasell 0e926ef3fd
allow configuration of Docker hostnames in bridge mode (#11173)
Add a new hostname string parameter to the network block which
allows operators to specify the hostname of the network namespace.
Changing this causes a destructive update to the allocation and it
is omitted if empty from API responses. This parameter also supports
interpolation.

In order to have a hostname passed as a configuration param when
creating an allocation network, the CreateNetwork func of the
DriverNetworkManager interface needs to be updated. In order to
minimize the disruption of future changes, rather than add another
string func arg, the function now accepts a request struct along with
the allocID param. The struct has the hostname as a field.

The in-tree implementations of DriverNetworkManager.CreateNetwork
have been modified to account for the function signature change.
In updating for the change, the enhancement of adding hostnames to
network namespaces has also been added to the Docker driver, whilst
the default Linux manager does not current implement it.
2021-09-16 08:13:09 +02:00

187 lines
5.8 KiB
Go

package allocrunner
import (
"context"
"fmt"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/plugins/drivers"
"github.com/miekg/dns"
)
const (
// dockerNetSpecLabelKey is the label added when we create a pause
// container to own the network namespace, and the NetworkIsolationSpec we
// get back from CreateNetwork has this label set as the container ID.
// We'll use this to generate a hostname for the task in the event the user
// did not specify a custom one. Please see dockerNetSpecHostnameKey.
dockerNetSpecLabelKey = "docker_sandbox_container_id"
// dockerNetSpecHostnameKey is the label added when we create a pause
// container and the task group network include a user supplied hostname
// parameter.
dockerNetSpecHostnameKey = "docker_sandbox_hostname"
)
type networkIsolationSetter interface {
SetNetworkIsolation(*drivers.NetworkIsolationSpec)
}
// allocNetworkIsolationSetter is a shim to allow the alloc network hook to
// set the alloc network isolation configuration without full access
// to the alloc runner
type allocNetworkIsolationSetter struct {
ar *allocRunner
}
func (a *allocNetworkIsolationSetter) SetNetworkIsolation(n *drivers.NetworkIsolationSpec) {
for _, tr := range a.ar.tasks {
tr.SetNetworkIsolation(n)
}
}
type networkStatusSetter interface {
SetNetworkStatus(*structs.AllocNetworkStatus)
}
// networkHook is an alloc lifecycle hook that manages the network namespace
// for an alloc
type networkHook struct {
// isolationSetter is a callback to set the network isolation spec when after the
// network is created
isolationSetter networkIsolationSetter
// statusSetter is a callback to the alloc runner to set the network status once
// network setup is complete
networkStatusSetter networkStatusSetter
// manager is used when creating the network namespace. This defaults to
// bind mounting a network namespace descritor under /var/run/netns but
// can be created by a driver if nessicary
manager drivers.DriverNetworkManager
// alloc should only be read from
alloc *structs.Allocation
// spec described the network namespace and is syncronized by specLock
spec *drivers.NetworkIsolationSpec
// networkConfigurator configures the network interfaces, routes, etc once
// the alloc network has been created
networkConfigurator NetworkConfigurator
// taskEnv is used to perform interpolation within the network blocks.
taskEnv *taskenv.TaskEnv
logger hclog.Logger
}
func newNetworkHook(logger hclog.Logger,
ns networkIsolationSetter,
alloc *structs.Allocation,
netManager drivers.DriverNetworkManager,
netConfigurator NetworkConfigurator,
networkStatusSetter networkStatusSetter,
taskEnv *taskenv.TaskEnv,
) *networkHook {
return &networkHook{
isolationSetter: ns,
networkStatusSetter: networkStatusSetter,
alloc: alloc,
manager: netManager,
networkConfigurator: netConfigurator,
taskEnv: taskEnv,
logger: logger,
}
}
func (h *networkHook) Name() string {
return "network"
}
func (h *networkHook) Prerun() error {
tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
if len(tg.Networks) == 0 || tg.Networks[0].Mode == "host" || tg.Networks[0].Mode == "" {
return nil
}
if h.manager == nil || h.networkConfigurator == nil {
h.logger.Trace("shared network namespaces are not supported on this platform, skipping network hook")
return nil
}
// Perform our networks block interpolation.
interpolatedNetworks := taskenv.InterpolateNetworks(h.taskEnv, tg.Networks)
// Interpolated values need to be validated. It is also possible a user
// supplied hostname avoids the validation on job registrations because it
// looks like it includes interpolation, when it doesn't.
if interpolatedNetworks[0].Hostname != "" {
if _, ok := dns.IsDomainName(interpolatedNetworks[0].Hostname); !ok {
return fmt.Errorf("network hostname %q is not a valid DNS name", interpolatedNetworks[0].Hostname)
}
}
// Our network create request.
networkCreateReq := drivers.NetworkCreateRequest{
Hostname: interpolatedNetworks[0].Hostname,
}
spec, created, err := h.manager.CreateNetwork(h.alloc.ID, &networkCreateReq)
if err != nil {
return fmt.Errorf("failed to create network for alloc: %v", err)
}
if spec != nil {
h.spec = spec
h.isolationSetter.SetNetworkIsolation(spec)
}
if created {
status, err := h.networkConfigurator.Setup(context.TODO(), h.alloc, spec)
if err != nil {
return fmt.Errorf("failed to configure networking for alloc: %v", err)
}
// If the driver set the sandbox hostname label, then we will use that
// to set the HostsConfig.Hostname. Otherwise, identify the sandbox
// container ID which will have been used to set the network namespace
// hostname.
if hostname, ok := spec.Labels[dockerNetSpecHostnameKey]; ok {
h.spec.HostsConfig = &drivers.HostsConfig{
Address: status.Address,
Hostname: hostname,
}
} else if hostname, ok := spec.Labels[dockerNetSpecLabelKey]; ok {
// the docker_sandbox_container_id is the full ID of the pause
// container, whereas we want the shortened name that dockerd sets
// as the pause container's hostname.
if len(hostname) > 12 {
hostname = hostname[:12]
}
h.spec.HostsConfig = &drivers.HostsConfig{
Address: status.Address,
Hostname: hostname,
}
}
h.networkStatusSetter.SetNetworkStatus(status)
}
return nil
}
func (h *networkHook) Postrun() error {
if h.spec == nil {
return nil
}
if err := h.networkConfigurator.Teardown(context.TODO(), h.alloc, h.spec); err != nil {
h.logger.Error("failed to cleanup network for allocation, resources may have leaked", "alloc", h.alloc.ID, "error", err)
}
return h.manager.DestroyNetwork(h.alloc.ID, h.spec)
}