Skip to content

Commit

Permalink
client: expose network namespace CNI config as task env vars.
Browse files Browse the repository at this point in the history
This change exposes CNI configuration details of a network
namespace as environment variables. This allows a task to use
these value to configure itself; a potential use case is to run
a Raft application binding to IP and Port details configured using
the bridge network mode.
  • Loading branch information
jrasell committed Jun 23, 2022
1 parent 854a2c6 commit 0a89baa
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 0 deletions.
6 changes: 6 additions & 0 deletions client/allocrunner/alloc_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,12 @@ func (ar *allocRunner) SetNetworkStatus(s *structs.AllocNetworkStatus) {
ar.stateLock.Lock()
defer ar.stateLock.Unlock()
ar.state.NetworkStatus = s.Copy()

// Iterate each task runner and add the status information. This allows the
// task to build the environment variables with this information available.
for _, tr := range ar.tasks {
tr.SetNetworkStatus(s.Copy())
}
}

func (ar *allocRunner) NetworkStatus() *structs.AllocNetworkStatus {
Expand Down
19 changes: 19 additions & 0 deletions client/allocrunner/taskrunner/task_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ type TaskRunner struct {
networkIsolationLock sync.Mutex
networkIsolationSpec *drivers.NetworkIsolationSpec

// allocNetworkStatus is provided from the allocrunner and allows us to
// include this information as env vars for the task. When manipulating
// this the allocNetworkStatusLock should be used.
allocNetworkStatusLock sync.Mutex
allocNetworkStatus *structs.AllocNetworkStatus

allocHookResources *cstructs.AllocHookResources

// serviceRegWrapper is the handler wrapper that is used by service hooks
Expand Down Expand Up @@ -1354,6 +1360,19 @@ func (tr *TaskRunner) SetNetworkIsolation(n *drivers.NetworkIsolationSpec) {
tr.networkIsolationLock.Unlock()
}

// SetNetworkStatus is called from the allocrunner to propagate the
// network status of an allocation. This call occurs once the network hook has
// run and allows this information to be exported as env vars within the
// taskenv.
func (tr *TaskRunner) SetNetworkStatus(s *structs.AllocNetworkStatus) {
tr.allocNetworkStatusLock.Lock()
tr.allocNetworkStatus = s
tr.allocNetworkStatusLock.Unlock()

// Update the taskenv builder.
tr.envBuilder = tr.envBuilder.SetNetworkStatus(s)
}

// triggerUpdate if there isn't already an update pending. Should be called
// instead of calling updateHooks directly to serialize runs of update hooks.
// TaskRunner state should be updated prior to triggering update hooks.
Expand Down
34 changes: 34 additions & 0 deletions client/taskenv/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ const (
// UpstreamPrefix is the prefix for passing upstream IP and ports to the alloc
UpstreamPrefix = "NOMAD_UPSTREAM_"

// AllocPrefix is a general purpose alloc prefix. It is currently used as
// the env var prefix used to export network namespace information
// including IP, Port, and interface.
AllocPrefix = "NOMAD_ALLOC_"

// VaultToken is the environment variable for passing the Vault token
VaultToken = "VAULT_TOKEN"

Expand Down Expand Up @@ -437,6 +442,9 @@ type Builder struct {
// and affect network env vars.
networks []*structs.NetworkResource

networkStatus *structs.AllocNetworkStatus
allocatedPorts structs.AllocatedPorts

// hookEnvs are env vars set by hooks and stored by hook name to
// support adding/removing vars from multiple hooks (eg HookA adds A:1,
// HookB adds A:2, HookA removes A, A should equal 2)
Expand Down Expand Up @@ -556,6 +564,12 @@ func (b *Builder) buildEnv(allocDir, localDir, secretsDir string,
// Build the Consul Connect upstream env vars
buildUpstreamsEnv(envMap, b.upstreams)

// Build the network namespace information if we have the required detail
// available.
if b.networkStatus != nil && b.allocatedPorts != nil {
addNomadAllocNetwork(envMap, b.allocatedPorts, b.networkStatus)
}

// Build the Vault Token
if b.injectVaultToken && b.vaultToken != "" {
envMap[VaultToken] = b.vaultToken
Expand Down Expand Up @@ -798,6 +812,7 @@ func (b *Builder) setAlloc(alloc *structs.Allocation) *Builder {

// Add any allocated host ports
if alloc.AllocatedResources.Shared.Ports != nil {
b.allocatedPorts = alloc.AllocatedResources.Shared.Ports
addPorts(b.otherPorts, alloc.AllocatedResources.Shared.Ports)
}
}
Expand Down Expand Up @@ -943,6 +958,13 @@ func (b *Builder) setUpstreamsLocked(upstreams []structs.ConsulUpstream) *Builde
return b
}

func (b *Builder) SetNetworkStatus(netStatus *structs.AllocNetworkStatus) *Builder {
b.mu.Lock()
defer b.mu.Unlock()
b.networkStatus = netStatus
return b
}

// buildUpstreamsEnv builds NOMAD_UPSTREAM_{IP,PORT,ADDR}_{destination} vars
func buildUpstreamsEnv(envMap map[string]string, upstreams []structs.ConsulUpstream) {
// Proxy sidecars always bind to localhost
Expand All @@ -961,6 +983,18 @@ func buildUpstreamsEnv(envMap map[string]string, upstreams []structs.ConsulUpstr
}
}

// addNomadAllocNetwork builds NOMAD_ALLOC_{IP,INTERFACE,ADDR}_{port_label}
// vars. NOMAD_ALLOC_PORT_* is handled within addPorts and therefore omitted
// from this function.
func addNomadAllocNetwork(envMap map[string]string, p structs.AllocatedPorts, netStatus *structs.AllocNetworkStatus) {
for _, allocatedPort := range p {
portStr := strconv.Itoa(allocatedPort.To)
envMap[AllocPrefix+"INTERFACE_"+allocatedPort.Label] = netStatus.InterfaceName
envMap[AllocPrefix+"IP_"+allocatedPort.Label] = netStatus.Address
envMap[AllocPrefix+"ADDR_"+allocatedPort.Label] = net.JoinHostPort(netStatus.Address, portStr)
}
}

// SetPortMapEnvs sets the PortMap related environment variables on the map
func SetPortMapEnvs(envs map[string]string, ports map[string]int) map[string]string {
if envs == nil {
Expand Down
52 changes: 52 additions & 0 deletions client/taskenv/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,58 @@ func TestEnvironment_Upstreams(t *testing.T) {
require.Equal(t, "1234", env["bar"])
}

func Test_addNetNamespacePort(t *testing.T) {
testCases := []struct {
inputPorts structs.AllocatedPorts
inputNetwork *structs.AllocNetworkStatus
expectedOutput map[string]string
name string
}{
{
inputPorts: structs.AllocatedPorts{
{Label: "http", To: 80},
},
inputNetwork: &structs.AllocNetworkStatus{
InterfaceName: "eth0",
Address: "172.26.64.11",
},
expectedOutput: map[string]string{
"NOMAD_ALLOC_INTERFACE_http": "eth0",
"NOMAD_ALLOC_IP_http": "172.26.64.11",
"NOMAD_ALLOC_ADDR_http": "172.26.64.11:80",
},
name: "single input port",
},
{
inputPorts: structs.AllocatedPorts{
{Label: "http", To: 80},
{Label: "https", To: 443},
},
inputNetwork: &structs.AllocNetworkStatus{
InterfaceName: "eth0",
Address: "172.26.64.11",
},
expectedOutput: map[string]string{
"NOMAD_ALLOC_INTERFACE_http": "eth0",
"NOMAD_ALLOC_IP_http": "172.26.64.11",
"NOMAD_ALLOC_ADDR_http": "172.26.64.11:80",
"NOMAD_ALLOC_INTERFACE_https": "eth0",
"NOMAD_ALLOC_IP_https": "172.26.64.11",
"NOMAD_ALLOC_ADDR_https": "172.26.64.11:443",
},
name: "multiple input ports",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
inputMap := make(map[string]string)
addNomadAllocNetwork(inputMap, tc.inputPorts, tc.inputNetwork)
assert.Equal(t, tc.expectedOutput, inputMap, tc.name)
})
}
}

func TestEnvironment_SetPortMapEnvs(t *testing.T) {
ci.Parallel(t)

Expand Down
24 changes: 24 additions & 0 deletions e2e/networking/networking.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,27 @@ func (tc *NetworkingE2ETest) TestNetworking_DockerBridgedHostnameInterpolation(f
f.NoError(err, "failed to run hostname exec command")
f.Contains(hostsOutput, "mylittlepony-0", "/etc/hosts doesn't contain hostname entry")
}

func (tc *NetworkingE2ETest) TestNetworking_DockerBridgedCNIEnvVars(f *framework.F) {

jobID := "test-networking-" + uuid.Generate()[0:8]
f.NoError(e2eutil.Register(jobID, "networking/inputs/docker_bridged_basic.nomad"))
tc.jobIDs = append(tc.jobIDs, jobID)
f.NoError(e2eutil.WaitForAllocStatusExpected(jobID, "default", []string{"running"}),
"job should be running with 1 alloc")

// Grab the allocations for the job.
allocs, _, err := tc.Nomad().Jobs().Allocations(jobID, false, nil)
f.NoError(err, "failed to get allocs for job")
f.Len(allocs, 1, "job should have one alloc")

// Run the env command within the allocation.
envOutput, err := e2eutil.AllocExec(allocs[0].ID, "sleep", "env", "default", nil)
f.NoError(err, "failed to run env exec command")

// Check all the network namespace env vars are present.
f.Contains("NOMAD_ALLOC_INTERFACE_http", envOutput, "namespace interface env var not found")
f.Contains("NOMAD_ALLOC_IP_http", envOutput, "namespace ip env var not found")
f.Contains("NOMAD_ALLOC_PORT_http", envOutput, "namespace port env var not found")
f.Contains("NOMAD_ALLOC_ADDR_http", envOutput, "namespace addr env var not found")
}

0 comments on commit 0a89baa

Please sign in to comment.