Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

networking: refactor building nomad bridge config #23772

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions client/allocrunner/cni/bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package cni
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a little weird having a separate package called cni here without it including much of any of the CNI configuration. There's no code isolation happening because everything is exported (has to be because of serialization.) All CNI setup including all the configuration tests are still in the client/allocrunner package. It seems hard to disentangle the "bridge configurator" code from CNI as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the initial motivation was to namespace the structs -- I found myself naming them like FirewallCNIPlugin just because they were in the allocrunner package, and I thought if I made a small new package, then they could be named more simply and still make sense. I also just like small, focused packages in general.

but were I to spend more time on this, I probably would move more of the CNI stuff into here, but I figure this is an incremental improvement to be perhaps built on further, later.

regarding the "networking" package comment, a decent chunk of our networking config, especially on linux, is CNI, but I might still like to distinguish the things that are specifically satisfying CNI spec from other more-general networking concerns. i.e. if I really went ham refactoring this area of allocrunner, I might make a "networking" package, then still put a (relatively smaller) "cni" package within there.

but as you might imagine, I am trying not to boil the ocean! 😅

all that said, I could move the new golden-file tests into here, since they are a direct output of this package. I left them where they were because that was the hook into this refactor, but I did create them here in the first place...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also admit to being a little concerned about backports, since I've been intending to go back to 1.6.x for future maintainability, but e.g. consul-cni is 1.8.x only. backports won't be tooooo painful anyway, or I could forego them entirely, but at any rate, it was on my mind when deciding what all to move into this new package.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but as you might imagine, I am trying not to boil the ocean!

Totally reasonable 👍


import "encoding/json"

// Conflist is the .conflist format of CNI network config.
type Conflist struct {
CniVersion string `json:"cniVersion"`
Name string `json:"name"`
Plugins []any `json:"plugins"`
}

// Json produces indented json of the conflist.
func (b Conflist) Json() ([]byte, error) {
return json.MarshalIndent(b, "", "\t")
}

// NomadBridgeConfig determines the contents of the Conflist.
type NomadBridgeConfig struct {
BridgeName string
AdminChainName string
IPv4Subnet string
HairpinMode bool
ConsulCNI bool
}

// NewNomadBridgeConflist produces a full Conflist from the config.
func NewNomadBridgeConflist(conf NomadBridgeConfig) Conflist {
// Update website/content/docs/networking/cni.mdx when the bridge config
// is modified. The json versions of the config can be found in
// client/allocrunner/test_fixtures/*.conflist.json
// If CNI plugins are added or versions need to be updated for new fields,
// add a new constraint to nomad/job_endpoint_hooks.go

ipRanges := [][]Range{
{{Subnet: conf.IPv4Subnet}},
}
ipRoutes := []Route{
{Dst: "0.0.0.0/0"},
}

plugins := []any{
Generic{
Type: "loopback",
},
Bridge{
Type: "bridge",
Bridgename: conf.BridgeName,
IpMasq: true,
IsGateway: true,
ForceAddress: true,
HairpinMode: conf.HairpinMode,
Ipam: IPAM{
Type: "host-local",
Ranges: ipRanges,
Routes: ipRoutes,
},
},
Firewall{
Type: "firewall",
Backend: "iptables",
AdminChainName: conf.AdminChainName,
},
Portmap{
Type: "portmap",
Capabilities: PortmapCapabilities{
Portmappings: true,
},
Snat: true,
},
}
if conf.ConsulCNI {
plugins = append(plugins, ConsulCNI{
Type: "consul-cni",
LogLevel: "debug",
})
}

return Conflist{
CniVersion: "0.4.0",
Name: "nomad",
Plugins: plugins,
}
}
31 changes: 31 additions & 0 deletions client/allocrunner/cni/bridge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package cni

import (
"testing"

"github.com/shoenig/test/must"
)

func TestConflist_Json(t *testing.T) {
conf := &Conflist{
CniVersion: "0.0.1",
Name: "test-config",
Plugins: []any{
Generic{Type: "test-plugin"},
},
}
bts, err := conf.Json()
must.NoError(t, err)
must.Eq(t, `{
"cniVersion": "0.0.1",
"name": "test-config",
"plugins": [
{
"type": "test-plugin"
}
]
}`, string(bts))
}
Comment on lines +12 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this just test the stdlib?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it tests stdlib json, but the main target is the appropriate struct tags to match CNI spec. I was aiming for a minimal test, but this is probably better covered by the more holistic real-world bridge conflists.

58 changes: 58 additions & 0 deletions client/allocrunner/cni/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package cni

// Generic has the one key that all plugins must have: "type"
type Generic struct {
Type string `json:"type"`
}

// Bridge is the subset of options that we use to configure the "bridge" plugin.
// https://www.cni.dev/plugins/current/main/bridge/
type Bridge struct {
Type string `json:"type"`
Bridgename string `json:"bridge"`
IpMasq bool `json:"ipMasq"`
IsGateway bool `json:"isGateway"`
ForceAddress bool `json:"forceAddress"`
HairpinMode bool `json:"hairpinMode"`
Ipam IPAM `json:"ipam"`
}
type IPAM struct {
Type string `json:"type"`
Ranges [][]Range `json:"ranges"`
Routes []Route `json:"routes"`
}
type Range struct {
Subnet string `json:"subnet"`
}
type Route struct {
Dst string `json:"dst"`
}

// Firewall is the "firewall" plugin.
// https://www.cni.dev/plugins/current/meta/firewall/
type Firewall struct {
Type string `json:"type"`
Backend string `json:"backend"`
AdminChainName string `json:"iptablesAdminChainName"`
}

// Portmap is the "portmap" plugin.
// https://www.cni.dev/plugins/current/meta/portmap/
type Portmap struct {
Type string `json:"type"`
Capabilities PortmapCapabilities `json:"capabilities"`
Snat bool `json:"snat"`
}
type PortmapCapabilities struct {
Portmappings bool `json:"portMappings"`
}

// ConsulCNI is the "consul-cni" plugin used for transparent proxy.
// https://github.com/hashicorp/consul-k8s/blob/main/control-plane/cni/main.go
type ConsulCNI struct {
Type string `json:"type"`
LogLevel string `json:"log_level"`
}
87 changes: 20 additions & 67 deletions client/allocrunner/networking_bridge_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/coreos/go-iptables/iptables"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/allocrunner/cni"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/plugins/drivers"
)
Expand Down Expand Up @@ -60,16 +61,24 @@ func newBridgeNetworkConfigurator(log hclog.Logger, alloc *structs.Allocation, b
}

var netCfg []byte
var err error

tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup)
for _, svc := range tg.Services {
if svc.Connect.HasTransparentProxy() {
netCfg = buildNomadBridgeNetConfig(*b, true)
netCfg, err = buildNomadBridgeNetConfig(*b, true)
if err != nil {
return nil, err
}

break
}
}
if netCfg == nil {
netCfg = buildNomadBridgeNetConfig(*b, false)
netCfg, err = buildNomadBridgeNetConfig(*b, false)
if err != nil {
return nil, err
}
}

parser := &cniConfParser{
Expand Down Expand Up @@ -156,69 +165,13 @@ func (b *bridgeNetworkConfigurator) Teardown(ctx context.Context, alloc *structs
return b.cni.Teardown(ctx, alloc, spec)
}

func buildNomadBridgeNetConfig(b bridgeNetworkConfigurator, withConsulCNI bool) []byte {
var consulCNI string
if withConsulCNI {
consulCNI = consulCNIBlock
}

return []byte(fmt.Sprintf(nomadCNIConfigTemplate,
b.bridgeName,
b.hairpinMode,
b.allocSubnet,
cniAdminChainName,
consulCNI,
))
}

// Update website/content/docs/networking/cni.mdx when the bridge configuration
// is modified. If CNI plugins are added or versions need to be updated for new
// fields, add a new constraint to nomad/job_endpoint_hooks.go
const nomadCNIConfigTemplate = `{
gulducat marked this conversation as resolved.
Show resolved Hide resolved
"cniVersion": "0.4.0",
"name": "nomad",
"plugins": [
{
"type": "loopback"
},
{
"type": "bridge",
"bridge": %q,
"ipMasq": true,
"isGateway": true,
"forceAddress": true,
"hairpinMode": %v,
"ipam": {
"type": "host-local",
"ranges": [
[
{
"subnet": %q
}
]
],
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
},
{
"type": "firewall",
"backend": "iptables",
"iptablesAdminChainName": %q
},
{
"type": "portmap",
"capabilities": {"portMappings": true},
"snat": true
}%s
]
func buildNomadBridgeNetConfig(b bridgeNetworkConfigurator, withConsulCNI bool) ([]byte, error) {
conf := cni.NewNomadBridgeConflist(cni.NomadBridgeConfig{
BridgeName: b.bridgeName,
AdminChainName: cniAdminChainName,
IPv4Subnet: b.allocSubnet,
HairpinMode: b.hairpinMode,
ConsulCNI: withConsulCNI,
})
return conf.Json()
}
`

const consulCNIBlock = `,
{
"type": "consul-cni",
"log_level": "debug"
}
`
19 changes: 11 additions & 8 deletions client/allocrunner/networking_bridge_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package allocrunner

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/hashicorp/nomad/ci"
Expand Down Expand Up @@ -51,16 +53,17 @@ func Test_buildNomadBridgeNetConfig(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
ci.Parallel(t)
bCfg := buildNomadBridgeNetConfig(*tc.b, tc.withConsulCNI)
bCfg, err := buildNomadBridgeNetConfig(*tc.b, tc.withConsulCNI)
must.NoError(t, err)

// Validate that the JSON created is rational
must.True(t, json.Valid(bCfg))
if tc.withConsulCNI {
must.StrContains(t, string(bCfg), "consul-cni")
} else {
must.StrNotContains(t, string(bCfg), "consul-cni")
}

// and that it matches golden expectations
goldenFile := filepath.Join("test_fixtures", tc.name+".conflist.json")
expect, err := os.ReadFile(goldenFile)
must.NoError(t, err)
must.Eq(t, string(expect), string(bCfg)+"\n")
})
}
}
44 changes: 44 additions & 0 deletions client/allocrunner/test_fixtures/bad_input.conflist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to ensure the refactor is no-op, these files were generated by the old code (plus whitespace), and passed prior to the refactor.

"cniVersion": "0.4.0",
"name": "nomad",
"plugins": [
{
"type": "loopback"
},
{
"type": "bridge",
"bridge": "bad\"",
"ipMasq": true,
"isGateway": true,
"forceAddress": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"ranges": [
[
{
"subnet": "172.26.64.0/20"
}
]
],
"routes": [
{
"dst": "0.0.0.0/0"
}
]
}
},
{
"type": "firewall",
"backend": "iptables",
"iptablesAdminChainName": "NOMAD-ADMIN"
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
},
"snat": true
}
]
}
Loading
Loading