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) }